Skip to content

guard IsLocalResourceName against names missing documents segment#16396

Open
isl-Ramzi wants to merge 2 commits into
firebase:mainfrom
isl-Ramzi:bundle-resource-name-documents-guard
Open

guard IsLocalResourceName against names missing documents segment#16396
isl-Ramzi wants to merge 2 commits into
firebase:mainfrom
isl-Ramzi:bundle-resource-name-documents-guard

Conversation

@isl-Ramzi

Copy link
Copy Markdown
Contributor

A bundle whose document name matches the local project and database but stops before the documents segment, such as projects/<id>/databases/<db>, passes IsLocalResourceName and then reaches PopFirst(5) on a four-segment path, which trips a HARD_ASSERT and aborts the process. The same predicate gates DecodeName and IsLocalDocumentKey (the latter reachable from a document field's referenceValue), and bundle payloads handed to loadBundle are untrusted. Require the five-segment documents prefix in IsLocalResourceName so short names are rejected the way ExtractLocalPathFromResourceName already rejects them.

@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@ncooke3

ncooke3 commented Jul 14, 2026

Copy link
Copy Markdown
Member

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates IsLocalResourceName in serializer.cc to ensure that the resource path contains at least 5 segments and that the fifth segment is "documents", preventing assertions when stripping the prefix. It also adds unit tests to verify that decoding fails when the "documents" segment is missing. The review feedback suggests using path.size() >= 5 instead of path.size() > 4 for consistency with other parts of the codebase, and improving the formatting of the JSON string in the new unit test.

Comment thread Firestore/core/src/remote/serializer.cc Outdated
Comment on lines +1238 to +1240
std::string json(
R"({"name":"projects/p/databases/default",
"readTime":{"seconds":"0","nanos":0}})");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

The JSON string in the raw string literal can be formatted with proper indentation to improve readability.

Suggested change
std::string json(
R"({"name":"projects/p/databases/default",
"readTime":{"seconds":"0","nanos":0}})");
std::string json(
R"({
"name": "projects/p/databases/default",
"readTime": {"seconds": "0", "nanos": 0}
})");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moot now — that test is gone on the rebase, since DecodeNonDocumentKeyResourceNameFails from #16221 already covers the same name.

@cherylEnkidu

Copy link
Copy Markdown
Contributor

Thank you for this PR! Adding the bounds and segment validation directly into Serializer::IsLocalResourceName is a very elegant solution to the root cause of this crash.

We currently have another PR in progress that we need to merge into the main branch first. Could we ask you to hold off on this PR until that one lands?

Once the other PR is merged, we will revisit this to get your holistic fix integrated. Thanks again for your excellent work on this!

@isl-Ramzi

Copy link
Copy Markdown
Contributor Author

Thank you for the update! No problem at all—I’ll hold off on this PR until the other one is merged into main. Once that's done, I'll be happy to rebase or make any necessary updates to help get this fix integrated. Thanks for the review, and I appreciate the feedback!

@cherylEnkidu

Copy link
Copy Markdown
Contributor

Hi @isl-Ramzi

The other PR has already been merged, so the file on the main branch has changed. Would you mind rebasing your branch onto the latest main and reapplying your changes?

Require the five-segment `projects/{p}/databases/{d}/documents` prefix in
`Serializer::IsLocalResourceName` so a shorter name such as
`projects/{p}/databases/{d}` cannot reach a `PopFirst(5)` on a four-segment
path. Valid document names always carry the `documents` segment, so
valid-input behavior is unchanged.

With the check at the predicate, the equivalent per-caller checks added in
firebase#16221 are redundant and are folded back into it:

  - `BundleSerializer::DecodeName` drops its size/segment test.
  - `Serializer::IsLocalDocumentKey` returns to
    `DocumentKey::IsDocumentKey(resource.PopFirst(5))`, now that the
    predicate guarantees the prefix.

Add a regression test for a `referenceValue` that stops before `documents`,
which reaches the predicate through `IsLocalDocumentKey`. The short-name case
for documents and document metadata is already covered by
`DecodeNonDocumentKeyResourceNameFails`.
@isl-Ramzi
isl-Ramzi force-pushed the bundle-resource-name-documents-guard branch from e34d62a to 5f00a2c Compare July 17, 2026 05:19
@isl-Ramzi

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main.

Now that #16221 has landed, the crash itself is already fixed at the call sites, so this is down to consolidating those checks into the predicate. Three things worth a look:

  • IsLocalResourceName now requires the five-segment documents prefix.
  • DecodeName drops its path.size() < 5 || path[4] != "documents" test, since the predicate covers it.
  • IsLocalDocumentKey goes back to DocumentKey::IsDocumentKey(resource.PopFirst(5)). The (size() - 5) % 2 == 0 form is equivalent once the prefix is guaranteed, and I checked the two agree on every shape I could think of (short name, root documents, collection path, foreign project/database), but happy to keep your version if you would rather not re-touch it.

I dropped my document-metadata test because DecodeNonDocumentKeyResourceNameFails already covers that name. The referenceValue one I kept, since that reaches the predicate through IsLocalDocumentKey and nothing else covers it.

If you would rather keep the checks where #16221 put them and close this out, that is fine by me too.

Comment thread Firestore/core/src/remote/serializer.cc Outdated
return IsLocalResourceName(resource) && resource.size() >= 5 &&
resource[4] == "documents" && (resource.size() - 5) % 2 == 0;
return IsLocalResourceName(resource) &&
DocumentKey::IsDocumentKey(resource.PopFirst(5));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we revert this to avoid the memory allocation from resource.PopFirst(5)? DocumentKey::IsDocumentKey only checks the size parity, so copying the path isn't necessary.

Also, since you added the size >= 5 and "documents" checks to IsLocalResourceName above, we can simplify this function down to just:

bool Serializer::IsLocalDocumentKey(absl::string_view path) const {
  auto resource = ResourcePath::FromStringView(path);
  return IsLocalResourceName(resource) && (resource.size() - 5) % 2 == 0;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, reverted to your non-allocating form. Since IsLocalResourceName now guarantees the five-segment documents prefix, the size parity check on its own is enough here, so IsLocalDocumentKey is back to just IsLocalResourceName(resource) && (resource.size() - 5) % 2 == 0. No more PopFirst copy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants