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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/clock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ name: Clock gates
# universe time, built with stock 0.9.12 beside it in an Ubuntu 24.04 image and run through tst/clock's gates on x86_64
# and arm64: every wait API in C, Python, Java and Node at several rates, jumps, cancelled waits, timerfd duplicates,
# condvar and semaphore stress, the waits again with futex_waitv refused, a stalled writer and a dead one, torn page
# reads, the marker, the environment race and Chromium launches. It runs on pull requests that touch the library, its
# tier or Chrome's pin, weekly, and on demand; it isn't a required check.
# reads, the marker, the environment race, the TLS shim 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:
Expand Down Expand Up @@ -47,12 +47,12 @@ jobs:
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.
# Chrome's archive and the tarballs, 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') }}
key: clock-downloads-${{ matrix.arch }}-${{ hashFiles('src/agent_env/browsers/envs/chromium/chromium.py', 'src/agent_env/computers/clock/build.sh', 'tst/clock/conftest.py') }}
- name: Clock gates
run: .venv/bin/python -m pytest tst/clock -rA --timeout=2400 --junitxml=junit-clock-${{ matrix.arch }}.xml
- name: No skips
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, forks while another thread closes a timerfd, and Chromium launched 40 times with it and 10 with stock, side by side, lateness held to `tst/clock/bounds.py`; 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, forks while another thread closes a timerfd, the TLS shim's certificate checks on real time (curl, Python and `openssl s_client` at 182 days back: live sites verify, expired and untrusted certificates don't; and two copies of OpenSSL in one process, the system's 3.0 and a 1.1, each checking with its own), and Chromium launched 40 times with it and 10 with stock, side by side, lateness held to `tst/clock/bounds.py`; needs Docker, and fetches Chrome's archive, upstream's tarball and OpenSSL 1.1.1w's 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 |
Expand Down
25 changes: 17 additions & 8 deletions src/agent_env/computers/clock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ What follows the universe, and what doesn't:
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.
- **Certificates:** OpenSSL checks a peer's certificates against real time, through the TLS shim (`tls_realtime.c`,
preloaded after the library), so a client that reaches a live site without the computer's egress proxy still
verifies it months from real time, and still refuses an expired or untrusted one. The shim sets
`X509_verify_cert`'s check time unless the caller set one, and calls the caller's own libcrypto. TLS stacks of
their own (Chromium's BoringSSL, GnuTLS, NSS, Java's) and static binaries aren't covered.
- **Who:** only the session user's processes (`AGENTENV_SESSION_UID`). A program started with `AGENTENV_REAL_TIME=1`
stays real; the marker is taken out of its environment block, so its children are faked.
- **Settings:** read once as the library loads, from `/etc/agentenv/clock.conf` (`clock.conf` is the default), or from
Expand All @@ -29,7 +34,8 @@ What follows the universe, and what doesn't:
| `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 |
| `build.sh` | builds `libfaketimeMT.so.1` from upstream's sha256-pinned tarball and the files above, and the shim's `libtls_realtime.so` | Apache-2.0 |
| `tls_realtime.c` | the TLS shim: OpenSSL's certificate checks on real time, preloaded after the library | 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 |

Expand All @@ -51,10 +57,13 @@ computed from a read a moment earlier) continues from where the page has it inst
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, forks
while another thread closes a timerfd, and Chromium launches. `.github/workflows/clock.yml` runs them on x86_64 and
arm64. The gates hold correctness strictly (no wait ends early, every wait returns what it should, nothing is lost or
torn) and lateness to `tst/clock/bounds.py`, a bound a broken library misses by seconds and a slow machine doesn't
reach. `tst/integration/computer/clock_desktop_test.py` composes the library into a real computer (the ubuntu-24.04
desktop, the reference driver and agentenv's chromium, every process preloading it), which must pass the conformance
probe at full; then, its page written as root, a shell, Chrome's JavaScript and the driver's input follow the universe
through 30 jumps, a rate change and a freeze, with Chrome, Xvfb and the desktop's panel running on.
while another thread closes a timerfd, the TLS shim (curl, Python and `openssl s_client` with the clock 182 days back:
live sites verify, two local servers show the check is on real time and expired and untrusted certificates are still
refused, and two copies of OpenSSL in one process, the system's 3.0 and a 1.1, each check with their own), and
Chromium launches. `.github/workflows/clock.yml` runs them on x86_64 and arm64. The gates hold correctness strictly
(no wait ends early, every wait returns what it should, nothing is lost or torn) and lateness to
`tst/clock/bounds.py`, a bound a broken library misses by seconds and a slow machine doesn't reach.
`tst/integration/computer/clock_desktop_test.py` composes the library into a real computer (the ubuntu-24.04 desktop,
the reference driver and agentenv's chromium, every process preloading it), which must pass the conformance probe at
full; then, its page written as root, a shell, Chrome's JavaScript and the driver's input follow the universe through
30 jumps, a rate change and a freeze, with Chrome, Xvfb and the desktop's panel running on.
4 changes: 3 additions & 1 deletion src/agent_env/computers/clock/build.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/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.
# real); and the TLS shim beside it (tls_realtime.c). Writes OUT/libfaketimeMT.so.1 and OUT/libtls_realtime.so, which
# /etc/ld.so.preload lists in that order. 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
Expand All @@ -18,3 +19,4 @@ 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/"
${CC:-cc} -std=gnu99 -O2 -fPIC -shared -Wall -Wextra -Werror -o "$out/libtls_realtime.so" "$here/tls_realtime.c" -ldl
115 changes: 115 additions & 0 deletions src/agent_env/computers/clock/tls_realtime.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* agent-env's TLS shim: OpenSSL checks a peer's certificates against real time, whatever the universe's clock says.
*
* Preloaded after the clock library (/etc/ld.so.preload). A program on universe time months from real time would find
* every live site's certificate not yet valid or expired. Most clients reach live sites through the computer's egress
* proxy, which hands them certificates valid at the universe date; this is for the ones that bypass it. OpenSSL's
* libssl verifies a chain with X509_verify_cert, which this defines first in the global scope: it sets the
* verification's check time to real time, read with a raw system call the clock library doesn't intercept, unless the
* caller set one, then calls OpenSSL's own, the one in the caller's own libcrypto, so a process holding two copies of
* OpenSSL (a bundled one beside the system's) verifies each with its own.
*
* Not covered: TLS stacks of their own (Chromium's BoringSSL, GnuTLS, NSS, Java's) and static binaries.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stddef.h>
#include <string.h>
#include <time.h>

typedef struct x509_store_ctx_st X509_STORE_CTX;
typedef struct X509_VERIFY_PARAM_st X509_VERIFY_PARAM;

/* OpenSSL's X509_V_FLAG_USE_CHECK_TIME (x509_vfy.h), the same in 1.1 and 3 */
#define TLS_USE_CHECK_TIME 0x2
#define TLS_SYSTEM_LIBCRYPTO "libcrypto.so.3"

/* OpenSSL's functions as one copy of it has them */
struct tls_openssl
{
int (*verify)(X509_STORE_CTX *);
void (*set_time)(X509_STORE_CTX *, unsigned long, time_t);
X509_VERIFY_PARAM *(*get0_param)(X509_STORE_CTX *);
unsigned long (*get_flags)(const X509_VERIFY_PARAM *);
};

int X509_verify_cert(X509_STORE_CTX *ctx);

/* the real time in seconds, read with the system call itself (the clock library intercepts libc's), 0 if it fails */
static long tls_real_time(void)
{
struct timespec ts = {0, 0};
#if defined(__x86_64__)
long ret;
__asm__ volatile("syscall" : "=a"(ret) : "0"(228L), "D"((long) CLOCK_REALTIME), "S"(&ts) : "rcx", "r11", "memory");
if (ret != 0) return 0;
#elif defined(__aarch64__)
register long x8 __asm__("x8") = 113;
register long x0 __asm__("x0") = CLOCK_REALTIME;
register long x1 __asm__("x1") = (long) &ts;
__asm__ volatile("svc 0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory");
if (x0 != 0) return 0;
#else
#error the TLS shim reads real time with a raw system call on x86_64 or aarch64
#endif
return ts.tv_sec;
}

/* ``name`` in ``scope`` (a handle, or RTLD_NEXT), never this file's own definition */
static void *tls_lookup(void *scope, const char *name)
{
void *found = dlsym(scope, name);
return found == (void *) X509_verify_cert ? NULL : found;
}

static void tls_take(void *scope, struct tls_openssl *out)
{
out->verify = (int (*)(X509_STORE_CTX *)) tls_lookup(scope, "X509_verify_cert");
out->set_time = (void (*)(X509_STORE_CTX *, unsigned long, time_t)) tls_lookup(scope, "X509_STORE_CTX_set_time");
out->get0_param = (X509_VERIFY_PARAM * (*)(X509_STORE_CTX *)) tls_lookup(scope, "X509_STORE_CTX_get0_param");
out->get_flags = (unsigned long (*)(const X509_VERIFY_PARAM *)) tls_lookup(scope, "X509_VERIFY_PARAM_get_flags");
}

/* OpenSSL as the object at ``caller`` sees it: from that object's own dependencies (libssl's libcrypto, whichever copy
it is); for a caller that isn't a shared library depending on one (a program calling it itself), the next definition
in the global scope; else the system's libcrypto where it was loaded privately (a Python extension's). Looked up on
every call, once a handshake: a copy of OpenSSL unloaded and another loaded at its address must not be handed the
first one's functions. */
static void tls_resolve(void *caller, struct tls_openssl *out)
{
Dl_info info;
void *handle;
memset(out, 0, sizeof *out);
if (dladdr(caller, &info))
{
if (info.dli_fname != NULL && info.dli_fname[0] != 0
&& (handle = dlopen(info.dli_fname, RTLD_NOW | RTLD_NOLOAD)) != NULL)
{
tls_take(handle, out);
dlclose(handle);
if (out->verify != NULL) return;
}
}
tls_take(RTLD_NEXT, out);
if (out->verify != NULL) return;
if ((handle = dlopen(TLS_SYSTEM_LIBCRYPTO, RTLD_NOW | RTLD_NOLOAD)) != NULL)
{
tls_take(handle, out);
dlclose(handle);
}
}

int X509_verify_cert(X509_STORE_CTX *ctx)
{
struct tls_openssl ssl;
tls_resolve(__builtin_return_address(0), &ssl);
if (ssl.verify == NULL) return -1;
if (ctx != NULL && ssl.set_time != NULL && ssl.get0_param != NULL && ssl.get_flags != NULL)
{
X509_VERIFY_PARAM *param = ssl.get0_param(ctx);
long now = tls_real_time();
if (param != NULL && !(ssl.get_flags(param) & TLS_USE_CHECK_TIME) && now > 0) ssl.set_time(ctx, 0, (time_t) now);
}
return ssl.verify(ctx);
}
19 changes: 15 additions & 4 deletions tst/clock/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# 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.
# stock 0.9.12 from the same tarball beside it, the gate programs in C, Python, Java and Node, Chrome for Testing at
# the browsers bundle's pin, and OpenSSL 1.1.1w's libraries, a second copy of OpenSSL for the TLS gate.
# tst/clock/conftest.py assembles its context, with the 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 \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -q --no-install-recommends build-essential patch curl openssl \
libssl-dev 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
Expand All @@ -20,9 +21,19 @@ RUN /opt/clock-src/build.sh /opt/clock/lib /tmp/lft.tgz \
&& 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 downloads/openssl-1.1.1w.tar.gz /tmp/openssl.tgz
RUN tar xzf /tmp/openssl.tgz -C /tmp && cd /tmp/openssl-1.1.1w \
&& ./config shared no-tests >/dev/null && make -s -j"$(nproc)" build_libs >/dev/null \
&& mkdir -p /opt/openssl-1.1/lib /opt/openssl-1.1/include \
&& cp -P libcrypto.so libcrypto.so.1.1 /opt/openssl-1.1/lib/ && cp -r include/openssl /opt/openssl-1.1/include/ \
&& cd / && rm -rf /tmp/openssl.tgz /tmp/openssl-1.1.1w
COPY programs /opt/clock/programs
RUN mkdir -p /opt/clock/bin /opt/clock/java && cd /opt/clock/programs \
&& for p in uwait jumpwaits condstress2 semstress envrace rtread torn cancel tfdalias forklock; do \
gcc -O2 -o /opt/clock/bin/$p $p.c -lpthread || exit 1; done \
&& mkdir -p /opt/clock/tls && gcc -O2 -fPIC -shared -o /opt/clock/tls/tlsverify3.so tlsverify.c -lcrypto \
&& gcc -O2 -fPIC -shared -I/opt/openssl-1.1/include -o /opt/clock/tls/tlsverify11.so tlsverify.c \
-L/opt/openssl-1.1/lib -lcrypto -Wl,-rpath,/opt/openssl-1.1/lib \
&& gcc -O2 -o /opt/clock/bin/tlscopies tlscopies.c -ldl \
&& javac -d /opt/clock/java UWait.java && chmod 755 marker.sh
COPY harness.py /opt/clock/harness.py
Loading
Loading