Skip to content

Size the provider auth headers dynamically and stop truncating parsed floats - #55

Merged
mason-sharp merged 2 commits into
mainfrom
fix/issue-31-fixed-buffers
Aug 11, 2026
Merged

Size the provider auth headers dynamically and stop truncating parsed floats#55
mason-sharp merged 2 commits into
mainfrom
fix/issue-31-fixed-buffers

Conversation

@dpage

@dpage dpage commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #31.

What the audit found

Ten fixed-length buffers exist in the extension. Eight are provably bounded; two had a path where unbounded input could reach them, and both failed by silently truncating.

The auth headers, and this one is reachable. provider_openai.c, provider_voyage.c and provider_gemini.c each built their header into char auth_header[512], which leaves roughly 489 bytes for the key after the prefix. Nothing held the key under that: provider_load_api_key() accepts a file up to MAX_API_KEY_FILE_SIZE, which is 4096, so the two bounds disagreed by a factor of eight. That is not a theoretical gap, because JWT-style bearer tokens for OpenAI-compatible gateways routinely exceed a thousand characters, and the failure mode would be a credential quietly cut in half producing an authentication error indistinguishable from a wrong key. They now use psprintf(), which matches how url is already built and freed a few lines above in each of the same three functions, and removes the second bound rather than merely raising it.

The float parser. provider_parse_float_array() read numeric literals into char value_buf[32], kept only the leading characters of anything longer, and passed that to atof(). A literal that long is not something a provider could legitimately send, since %.17g never exceeds 24 characters, but the failure mode was to score on a number of an entirely different magnitude in silence. It now stops instead, and since all three callers already treat a short count as a dimension mismatch, that turns a silent wrong answer into a reported bad response with no signature change.

What was deliberately left alone

The other eight are bounded by construction, and the reasoning is recorded here so the next audit does not have to rederive it:

  • the BM25 term keys in bm25.c and bm25.h cannot be reached by anything overlong since Drop terms too long for the BM25 hash key #50 added the guard dropping such terms at tokenization, which is what made those keys safe;
  • the three dbname[NAMEDATALEN] buffers in worker.c are sized to the bound PostgreSQL itself places on a database name, so truncation cannot occur;
  • the worker's captured error message is diagnostic text, deliberately bounded so the copy can outlive the transaction abort, and the untruncated original has already reached the server log via EmitErrorReport(). The comment now says so explicitly.

Test plan

  • 19 regression tests and all TAP suites green on PostgreSQL 18.4, clean build with no new warnings

No new tests, said plainly rather than dressed up. Neither change has a test seam worth building. Exercising the auth header needs a live endpoint that will echo the header back, and the float parser is not reachable from SQL, so covering either would mean standing up a stub HTTP server purely to assert a string length. The auth header change is also a strict improvement regardless of input: psprintf() cannot truncate at any length, so there is no boundary left to test.

Audit of every fixed-length buffer in the extension. Two of the ten had a
path where an input of unbounded length could reach them, and both failed
by silently truncating rather than by complaining.

The auth headers in the OpenAI, Voyage and Gemini providers were built into
a 512 byte stack buffer, which leaves about 489 for the key itself. Nothing
kept the key under that: provider_load_api_key() accepts a file of up to
MAX_API_KEY_FILE_SIZE, which is 4096, so the two bounds disagreed by a
factor of eight. That gap is reachable rather than theoretical, because
JWT-style bearer tokens for OpenAI-compatible gateways routinely run past a
thousand characters. The result would have been a credential quietly cut in
half and an authentication failure indistinguishable from a wrong key.
These now use psprintf(), matching how the url in the same functions is
already built and freed, which removes the second bound altogether.

provider_parse_float_array() read numeric literals into a 32 byte buffer
and kept only the leading characters of anything longer, then handed that
to atof(). A run that long is not a number any provider could legitimately
send, since %.17g never exceeds 24 characters, but the failure mode was to
score on a value of an entirely different magnitude without a word. It now
stops instead, and because all three callers treat a short count as a
dimension mismatch, the malformed response is reported as one.

The remaining seven buffers are provably bounded and are left alone. The
BM25 term keys cannot be reached by anything too long since #50 added the
guard that drops such terms at tokenization; the three dbname buffers are
NAMEDATALEN, which is the bound PostgreSQL itself puts on a database name;
and the worker's captured error message is diagnostic text whose full
version has already reached the server log, which is now said explicitly in
the comment so the next audit need not rederive it.

Closes #31
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The float-array parser now rejects oversized numeric literals with PROVIDER_PARSE_MALFORMED and preserves the offending input position. Gemini, Ollama, and shared embedding parsing report malformed numeric errors separately from dimension mismatches. Gemini, OpenAI, and Voyage authentication headers now use dynamic allocation and are freed on failed and successful requests. Worker diagnostics document bounded error-message handling and server-log preservation.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary changes: dynamic provider authentication headers and rejection of oversized parsed floats.
Description check ✅ Passed The description explains the buffer audit, the two fixes, retained bounded buffers, and test results.
Linked Issues check ✅ Passed The changes satisfy issue #31 by dynamically sizing unbounded authentication headers and rejecting oversized numeric literals without silent truncation.
Out of Scope Changes check ✅ Passed The changed files and worker comment support the fixed-buffer audit and remain within the scope of issue #31.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-31-fixed-buffers

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · -2 duplication

Metric Results
Complexity 0
Duplication -2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

A rejected literal returned a short count, which every caller reports as
"Dimension mismatch" -- pointing the operator at their model or dimension
configuration rather than at a malformed response. Return a distinguished
value instead and give it its own message.

A sentinel rather than an error_msg out-param, since error_msg is only
initialised at the top-level entry points. It also fails safe: -1 can
never equal a dimension, so a missed call site still rejects.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/provider_common.c (1)

255-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add boundary regression coverage for PROVIDER_PARSE_MALFORMED.

Test the maximum accepted literal length and the first rejected length. Verify that the rejected case returns PROVIDER_PARSE_MALFORMED without advancing *pos past the offending run. Add one provider-level assertion that this result produces the malformed-numeric error instead of a dimension mismatch. The PR objective makes this boundary part of the parser contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/provider_common.c` around lines 255 - 268, Add regression tests around
the numeric parsing boundary exercised by the value-buffer check: verify the
maximum accepted literal length succeeds and the first longer length returns
PROVIDER_PARSE_MALFORMED without advancing *pos beyond the offending run. Add a
provider-level assertion confirming this result reports the malformed-numeric
error rather than a dimension mismatch, using the existing parser and provider
test helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/provider_common.c`:
- Around line 255-268: Add regression tests around the numeric parsing boundary
exercised by the value-buffer check: verify the maximum accepted literal length
succeeds and the first longer length returns PROVIDER_PARSE_MALFORMED without
advancing *pos beyond the offending run. Add a provider-level assertion
confirming this result reports the malformed-numeric error rather than a
dimension mismatch, using the existing parser and provider test helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2e8a558e-30f8-43da-8a5b-ab4ad92f0653

📥 Commits

Reviewing files that changed from the base of the PR and between 24d0611 and 9aa568b.

📒 Files selected for processing (7)
  • src/provider_common.c
  • src/provider_common.h
  • src/provider_gemini.c
  • src/provider_ollama.c
  • src/provider_openai.c
  • src/provider_voyage.c
  • src/worker.c

@mason-sharp
mason-sharp merged commit 107816d into main Aug 11, 2026
9 checks passed
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.

Audit fixed-length stack buffers across the extension

2 participants