Skip to content

Name the gates the profiler token actually holds - #376

Merged
diolektor merged 1 commit into
mainfrom
fix/profiler-auth-token-row-names-php-functions-that-do-not-exist
Sep 20, 2026
Merged

diolektor merged 1 commit into
mainfrom
fix/profiler-auth-token-row-names-php-functions-that-do-not-exist

Conversation

@diolektor

Copy link
Copy Markdown
Contributor

Problem

The PROFILER_AUTH_TOKEN row in docs/operations/configuration.md read:

Optional bearer token. When set, the oxphp_profiler_* PHP functions require requests carrying this token to enable on-demand profiling

Two errors in one row, both present in the released 0.11.0 text.

There is no oxphp_profiler_* PHP function. The userland API is seven functions in the OxPHP\Profile namespace (src/plugins/ox_profiler/php_sdk.rs:32,48,68,77,85,95,111). The oxphp_profiler_ prefix belongs to Prometheus metric names and internal C/Rust symbols — 88 hits repo-wide, not one of them a PHP function, and no near-miss spelling (oxphp_profile*, profiler_start) exists either. This row was the only place in docs/ that presented the prefix as userland PHP. The neighbouring oxphp_apm_*() row is not the same mistake: those functions really are flat (src/plugins/ox_apm/php_sdk.rs), and the profiler row looks like that template applied to a namespaced API.

The token does not stand in front of the PHP functions. register_functions takes _enabled and never reads it (php_sdk.rs:21-28); registration runs at mod.rs:390, ahead of the if !self.enabled gate at :438; and OxPHP\Profile\start() reads neither config nor headers. The token is consulted in exactly two places: validate_token for the three explicit triggers (trigger.rs:124-138) and the bearer check on the internal server's /__profiler/* routes (routes.rs:71-86).

What that costs a reader: searching their own code for oxphp_profiler_* returns nothing, and believing the token closes PHP-side activation means treating an open door as closed — set the token, and runs_total{source="sdk"} still counts every request whose script calls start(), not only the ones carrying it.

What the row says now

Secret the explicit triggers must carry — the X-OxPHP-Profile header, the OXPROF cookie, the __oxprof query parameter — and the bearer token for the /__profiler/* routes on the internal server. Unset or empty = no token required: any non-empty trigger value passes, and the /__profiler/* routes answer without a bearer. PROFILER_SAMPLE_RATE sampling and the OxPHP\Profile\* PHP functions never consult it — under PROFILER_ENABLED=true a script that calls OxPHP\Profile\start() profiles its own request with no token involved

Three things were added beyond removing the false names. The unset-or-empty behaviour, because config.rs:91-97 collapses both to None, after which validate_token accepts any non-empty trigger value and check_auth demands no bearer — the old row said only "when set" and left the other side of the branch to guesswork. The two non-gates, because the second error was an attributed gate, and naming the real ones without denying the false one leaves the reader's wrong belief intact. And the PROFILER_ENABLED=true qualifier on the SDK clause: a first draft promised start() profiles its own request unconditionally, which is not so — init returns before installing the request-end observer, the run store and the routes when the master switch is off, while the SDK functions are registered earlier and unconditionally, so calls succeed and collect nothing.

Wording follows docs/features/profiling.md:222, which already stated this correctly, rather than inventing a third formulation.

Guard

The diff adds no branch, so most of the row is verified by reading and by tests that already exist:

Row's claim Held by
the three triggers require the token trigger.rs::test_token_required_and_correct, _incorrect, _length_mismatch_rejected
with no token any non-empty value passes test_header_activates_without_token, _cookie_, _query_
/__profiler/* demands a bearer when a token is set profiler_routes_tests::auth_enforced_on_stats_when_token_configured
the SDK does not consult it tests/php/profiler/test_source_sdk.php, which runs under PROFILER_AUTH_TOKEN=test-token with no trigger and asserts start() profiled the request
sampling does not consult it nothing — see below

That last row is why this PR carries a test. Both existing sample-rate tests run with auth_token = None, so nothing held sampling to ignoring a configured token: a token check moved ahead of the draw would have left them green while making the new sentence false. test_sample_rate_ignores_a_configured_token configures a token, sends a request carrying no trigger at all, and asserts the activation source is still SampleRate. Mutant check: inserting if cfg.auth_token.is_some() { return None; } before the sampling branch reddens exactly this test and leaves the two existing ones passing.

What is deliberately not touched

The PROFILER_INTERNAL row directly above it. It is separately wrong, but fixing it needs a product decision about what the knob is supposed to mean, and that does not belong in a one-row correction.

§17 of docs/features/profiling.md still does not say that an unset token accepts any non-empty trigger value. The new row says it on the configuration page; the feature page's own hardening section is a wider change than this.

One pre-existing divergence is worth flagging for whoever reviews the last clause, since that clause rides on it: PROFILER_ENABLED is parsed by the Rust boolean parser (on/true/1/yes, case-insensitive) when the plugin boots, but ext/oxphp_sapi.c:7513-7519 registers the observer only on a case-sensitive "true" or "1". PROFILER_ENABLED=yes therefore boots the plugin, answers the routes, reports enabled on /config, and collects no span. Tracked separately, untouched here.

Verification

  • cargo fmt -- --check, cargo clippy --no-default-features -- -D warnings — clean
  • cargo test --no-default-features, and with plugin-apm,plugin-async,plugin-profiler,plugin-shared — 1718 unit, 1864 including the integration binaries, 0 failed
  • scripts/gen-llms-txt.sh && scripts/gen-llms-txt.sh --check — up to date (58 pages); llms-full.txt is regenerated, and its diff is the same single line
  • grep -rn 'oxphp_profiler_' over the repo — 88 hits, all metric names or internal symbols; grep -rn PROFILER_AUTH_TOKEN docs/ README.md CHANGELOG.md src/ — every remaining description names the same gates as the new row
  • No product code changed, so the Docker and worker-suite rows of the usual matrix do not apply

Docs:
  - The configuration reference described `PROFILER_AUTH_TOKEN` through `oxphp_profiler_*` PHP functions, of which the product has none — the userland API is `OxPHP\Profile\*`, and that prefix belongs to Prometheus metrics and internal symbols — and it put the token in front of enabling profiling from PHP, where it has never stood. The row now names what it does gate (the three explicit triggers, the bearer on the `/__profiler/*` routes), what an unset or empty value means for each, and that neither sampling nor the PHP functions consult it. An operator who set the token and read the old row expected `runs_total{source="sdk"}` to count only requests carrying it; it counts every one.

Tests:
  - Pin the sampling half of that claim. The two existing sample-rate tests run with no token configured, so nothing held sampling to ignoring one — a token check moved ahead of the draw would have left them green.

1718 unit tests, 1864 with the integration binaries.
@diolektor
diolektor merged commit 8257a2f into main Sep 20, 2026
7 checks passed
@diolektor
diolektor deleted the fix/profiler-auth-token-row-names-php-functions-that-do-not-exist branch September 20, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant