Skip to content

Conversation

@RossComputerGuy
Copy link
Member

@RossComputerGuy RossComputerGuy commented Oct 31, 2025

Motivation

Need a way to get the path of a Flake when it is not local.

Context

Summary by CodeRabbit

  • New Features
    • Added a C API capability to read a file located relative to a locked flake and return its contents via a caller-supplied callback.
    • The returned string is borrowed only for the duration of the callback invocation.
    • This extends flake file-reading support without changing existing API behavior or error-reporting contracts.

@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

Walkthrough

Adds a new C API function nix_locked_flake_read_path that reads a file located relative to the parent of a locked flake's path and returns its contents to the caller via a string callback, using the existing context-based error handling pattern.

Changes

Cohort / File(s) Summary
C API: read file from locked flake
src/libflake-c/nix_api_flake.h, src/libflake-c/nix_api_flake.cc
Adds nix_locked_flake_read_path(nix_c_context *context, nix_locked_flake *lockedFlake, const char *path, nix_get_string_callback callback, void *user_data). Implementation clears context errors, resolves the source path relative to the locked flake parent, reads the file via source_path.readFile(), invokes the provided callback with the file contents (borrowed during call), and maps exceptions to nix_err using existing macros/patterns.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as C caller
    participant CAPI as libflake-c API
    participant Flake as LockedFlake / SourcePath
    rect rgba(135,206,250,0.12)
    Note over CAPI,Flake: New flow: read file relative to locked flake parent
    end
    Caller->>CAPI: nix_locked_flake_read_path(ctx, lockedFlake, path, cb, ud)
    CAPI->>CAPI: clear context errors
    CAPI->>Flake: resolve parent + append path
    CAPI->>Flake: readFile()
    alt read success
        Flake-->>CAPI: contents (string)
        CAPI-->>Caller: invoke cb(contents, ud)
        CAPI-->>Caller: return NIX_OK
    else read error / exception
        Flake-->>CAPI: throw / error
        CAPI->>CAPI: map to nix_err via NIXC_CATCH_ERRS
        CAPI-->>Caller: return error code
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Areas to inspect:
    • Correct resolution of the locked flake parent and path-joining edge cases.
    • Lifetime/ownership of the string passed to the callback (borrowed-only contract).
    • Proper use of existing error-clearing and exception-to-nix_err mapping macros.

Suggested reviewers

  • edolstra

Poem

🐰 I hopped to the flake and peeked near its root,

Found a small file and gave it a hoot.
I passed you the bytes with a callback so neat,
Borrowed for now — a quick tasty treat. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new C API function for reading flake files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch RossComputerGuy/flake-path-capi

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23fae53 and b04856b.

📒 Files selected for processing (2)
  • src/libflake-c/nix_api_flake.cc (1 hunks)
  • src/libflake-c/nix_api_flake.h (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/libflake-c/nix_api_flake.cc
  • src/libflake-c/nix_api_flake.h
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build_x86_64-linux / manual
  • GitHub Check: build_x86_64-linux / vm_tests_smoke
  • GitHub Check: build_x86_64-linux / test
  • GitHub Check: build_aarch64-darwin / build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Oct 31, 2025

@github-actions github-actions bot temporarily deployed to pull request October 31, 2025 18:22 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 236ebef and 641c031.

📒 Files selected for processing (2)
  • src/libflake-c/nix_api_flake.cc (1 hunks)
  • src/libflake-c/nix_api_flake.h (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/libflake-c/nix_api_flake.h (1)
src/libflake-c/nix_api_flake.cc (2)
  • nix_locked_flake_get_path (209-218)
  • nix_locked_flake_get_path (209-210)
src/libflake-c/nix_api_flake.cc (1)
src/libutil-c/nix_api_util.cc (4)
  • nix_clear_err (64-68)
  • nix_clear_err (64-64)
  • call_nix_get_string_callback (156-160)
  • call_nix_get_string_callback (156-156)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build_x86_64-linux / vm_tests_smoke
  • GitHub Check: build_x86_64-linux / test
  • GitHub Check: build_x86_64-linux / manual
  • GitHub Check: build_aarch64-darwin / build
  • GitHub Check: eval
🔇 Additional comments (1)
src/libflake-c/nix_api_flake.cc (1)

209-218: Verify the .parent() call aligns with the intended API behavior.

The implementation calls .parent() on line 214, returning the parent directory of the flake path rather than the flake path itself. Given:

  • The function is named nix_locked_flake_get_path
  • The PR objective states: "obtain the path of a Flake when the Flake is not local"

Please confirm whether returning the parent directory is intentional. If users need the actual flake path (e.g., /path/to/flake instead of /path/to), consider removing the .parent() call:

-        auto v = lockedFlake->lockedFlake->flake.path.parent().to_string();
+        auto v = lockedFlake->lockedFlake->flake.path.to_string();

If the parent directory is indeed the intended return value, update the function documentation in the header file accordingly.

Comment on lines 241 to 257
/**
* @brief Get the root path of a flake.
* @note The callback borrows the string only for the duration of the call.
*
* @param[out] context Optional, stores error information
* @param[in] locked_flake the flake to get the path for
* @param[in] callback The callback to call with the string
* @param[in] user_data Additional data to pass for the callback
*/
nix_err nix_locked_flake_get_path(
nix_c_context * context, nix_locked_flake * lockedFlake, nix_get_string_callback callback, void * user_data);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify documentation: "root path" vs "parent directory".

The documentation states "Get the root path of a flake," but the implementation at line 214 in src/libflake-c/nix_api_flake.cc calls .parent(), which returns the parent directory of the flake path, not the flake path itself.

Please clarify the documentation to accurately reflect what the function returns. If the intent is to return the directory containing the flake, consider updating the doc comment to:

 /**
- * @brief Get the root path of a flake.
+ * @brief Get the parent directory path of a flake.
  * @note The callback borrows the string only for the duration of the call.

Alternatively, if the function should return the flake path itself (not the parent), remove the .parent() call in the implementation.

🤖 Prompt for AI Agents
In src/libflake-c/nix_api_flake.h around lines 241-252, the doc comment
incorrectly says "root path of a flake" while the implementation returns the
parent directory; update the comment to state it returns the parent directory
containing the flake (e.g., "Get the parent directory containing the flake") and
clarify the borrowing semantics, or if the intended behavior is to return the
flake path itself, instead modify the implementation to return the flake path
(remove the .parent() call) and adjust tests/usage accordingly so the header and
implementation match.

@edolstra edolstra changed the title C API: add nix_locked_flake_get_path for flake path retreival C API: add nix_locked_flake_get_path for flake path retrieval Nov 3, 2025
nix_c_context * context, nix_flake_settings * settings, EvalState * evalState, nix_locked_flake * lockedFlake);

/**
* @brief Get the root path of a flake.
Copy link
Collaborator

@edolstra edolstra Nov 3, 2025

Choose a reason for hiding this comment

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

Note that when lazy trees are enabled, flake.path is a virtual (i.e. random) path that doesn't actually exist. So this is probably not a useful thing to expose.

Depending on the intended use, you could:

  • Copy the path to the Nix store by calling state.devirtualize(). This can be expensive.
  • Create C bindings for the SourceAccessor / SourcePath API to allow the caller to access the flake lazily.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, that's a good point. Is there a way to detect when lazy trees is active? I know I somewhat need a real file since I'm calling std::fs::read_to_string() within rust.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be better then to provide an API to return the contents of a file in a flake (i.e. wrapping SourcePath::readFile()). Then it works for the lazy and non-lazy case. E.g. to read flake.lock you would do

nix_locked_flake_get_path(context, lockedFlake, "/flake.lock", callback, ...);

where callback is called with the contents of the file.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, that can work

Copy link
Member Author

Choose a reason for hiding this comment

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

Alright, done

@RossComputerGuy RossComputerGuy force-pushed the RossComputerGuy/flake-path-capi branch from 641c031 to 23fae53 Compare November 3, 2025 18:13
@RossComputerGuy RossComputerGuy changed the title C API: add nix_locked_flake_get_path for flake path retrieval C API: add nix_locked_flake_read_path for flake file reading Nov 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/libflake-c/nix_api_flake.cc (1)

209-223: Add documentation for this new API function.

The function lacks documentation explaining:

  • Its purpose (read file contents relative to the flake's parent directory)
  • The expected format/constraints of the path parameter
  • Why parent() is used instead of the flake path directly
  • Example usage or common use cases

Adding inline comments or header documentation would improve clarity for API consumers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 641c031 and 23fae53.

📒 Files selected for processing (2)
  • src/libflake-c/nix_api_flake.cc (1 hunks)
  • src/libflake-c/nix_api_flake.h (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/libflake-c/nix_api_flake.h
🧰 Additional context used
🧬 Code graph analysis (1)
src/libflake-c/nix_api_flake.cc (1)
src/libutil-c/nix_api_util.cc (4)
  • nix_clear_err (64-68)
  • nix_clear_err (64-64)
  • call_nix_get_string_callback (156-160)
  • call_nix_get_string_callback (156-156)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_aarch64-darwin / build

Comment on lines 209 to 223
nix_err nix_locked_flake_read_path(
nix_c_context * context,
nix_locked_flake * lockedFlake,
const char * path,
nix_get_string_callback callback,
void * user_data)
{
nix_clear_err(context);
try {
auto source_path = lockedFlake->lockedFlake->flake.path.parent() / path;
auto v = source_path.readFile();
return call_nix_get_string_callback(v, callback, user_data);
}
NIXC_CATCH_ERRS
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify the function's purpose—PR title/motivation vs. implementation mismatch.

The PR title mentions "nix_locked_flake_get_path" and the motivation states "obtain the path of a Flake," but this implementation is named "nix_locked_flake_read_path" and reads file contents rather than returning a path. This creates confusion about the intended API:

  • If the goal is to get the flake's path, the function should return/callback with the path string itself, not read a file.
  • If the goal is to read a file relative to the flake, then the PR title and motivation need updating to match.

Please clarify which behavior is intended and ensure the PR description, function name, and implementation are aligned.

🤖 Prompt for AI Agents
In src/libflake-c/nix_api_flake.cc around lines 209-223, the function name and
implementation disagree with the PR title/motivation: either rename the function
to reflect that it reads file contents (e.g.,
nix_locked_flake_read_file/read_path_file) and update the PR title/description
and API docs to state it returns file contents, or change the implementation so
it returns the flake path string (call the callback with
lockedFlake->lockedFlake->flake.path.string()) and rename it to
nix_locked_flake_get_path; update related header declarations, tests, and any
callers to match the chosen behavior so name, docs, and implementation are
consistent.

{
nix_clear_err(context);
try {
auto source_path = lockedFlake->lockedFlake->flake.path.parent() / path;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Address path traversal risk and clarify parent() usage.

Line 218 constructs the file path without validating the path parameter, which could allow path traversal attacks if callers pass values like "../../etc/passwd". Additionally, using .parent() on the flake path is unclear:

  • Why resolve relative to the parent directory instead of the flake directory itself?
  • What is the expected use case for this behavior?

Consider:

  1. Validating/sanitizing the path parameter to prevent traversal outside the intended directory.
  2. Documenting why parent() is used, or using lockedFlake->lockedFlake->flake.path directly if that's the intended base.

To verify if path validation/canonicalization utilities exist in the codebase that could be applied here:


🏁 Script executed:

#!/bin/bash
# Search for path validation or canonicalization patterns
rg -nP --type=cpp -C3 '\b(canonPath|realPath|isInDir|sanitize.*[Pp]ath)\s*\('

Length of output: 18113


I need to examine the context around line 218 in the file to understand the parameter flow and usage pattern:

Now I need to examine the specific context around line 218 in nix_api_flake.cc:

I need to examine the actual nix_api_flake.cc file to understand the function context. Let me run a shell script:

This search will help me locate the exact function and context. However, since I don't have direct execution capability, let me try a different approach by examining if the file and function can be found through web search specific to the codebase issue:

Based on my analysis of the shell script output from the initial search and the web research, I can now provide my findings:

Based on my comprehensive verification:

Confirm path traversal vulnerability concern and recommend validation.

Path validation utilities (canonPath, isInDir, isDirOrInDir) are confirmed available in src/libutil/include/nix/util/file-system.hh and actively used throughout the codebase. The pattern of validating paths before use exists in src/libstore/unix/build/derivation-builder.cc (lines 878-886), where paths are canonicalized and checked against allowlists using isDirOrInDir().

Your concern about line 218 is valid—without validation of the path parameter, a caller could pass values like "../../etc/passwd" to traverse outside the intended directory. The recommended approach is to employ weakly_canonical() or canonPath() to normalize the path and then check that the result is prefixed with your base path using isInDir().

The use of .parent() on the locked flake path is also worth clarifying in a comment—document why the parent directory is the intended base instead of the flake directory itself.


🏁 Script executed:

[run_scripts]
#!/bin/bash
# Search for the function containing line 218 in nix_api_flake.cc
rg -n --type=cpp "source_path.*parent\(\)" src/libflake-c/nix_api_flake.cc -B 20 -A 5

Length of output: 945


Now let me check if there are similar patterns in the same file:

Based on my complete verification:

Fix critical path traversal vulnerability in nix_locked_flake_read_path.

The vulnerability is confirmed. The function accepts user-supplied path parameter and directly concatenates it with lockedFlake->lockedFlake->flake.path.parent() before calling readFile(), with no validation. This allows attackers to use sequences like "../../etc/passwd" to read arbitrary files.

The codebase provides validation utilities: canonPath() and isInDir() (available in src/libutil/include/nix/util/file-system.hh). The established pattern used elsewhere (e.g., src/libstore/unix/build/derivation-builder.cc lines 878-886) demonstrates how to apply these protections.

Recommended fix:

  1. Canonicalize the path using canonPath()
  2. Verify the result stays within the flake directory using isInDir()
  3. Document why .parent() is used instead of the flake directory itself

@github-actions github-actions bot temporarily deployed to pull request November 3, 2025 18:18 Inactive
{
nix_clear_err(context);
try {
auto source_path = lockedFlake->lockedFlake->flake.path.parent() / path;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
auto source_path = lockedFlake->lockedFlake->flake.path.parent() / path;
auto source_path = lockedFlake->lockedFlake->flake.path.parent() / CanonPath(path);

Otherwise I think it may select the wrong overload for the / operator, namely:

    /**
     * Append a single component `c` to this path. `c` must not
     * contain a slash. A slash is implicitly added between this path
     * and `c`.
     */
    SourcePath operator/(std::string_view c) const;

so appending a path with multiple components (foo/bar.nix) wouldn't work.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, I forgot about that.

@RossComputerGuy RossComputerGuy force-pushed the RossComputerGuy/flake-path-capi branch from 23fae53 to b04856b Compare November 4, 2025 16:49
@github-actions github-actions bot temporarily deployed to pull request November 4, 2025 16:51 Inactive
@edolstra edolstra added this pull request to the merge queue Nov 6, 2025
Merged via the queue into main with commit 5457686 Nov 6, 2025
35 checks passed
@edolstra edolstra deleted the RossComputerGuy/flake-path-capi branch November 6, 2025 22:11
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.

3 participants