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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ members = [
"crates/cairo-program-runner-lib",
"crates/stwo_run_and_prove",
"crates/vm_runner",
"crates/concat-aggregator",
"crates/cairo-compile-utils",
]
resolver = "2"

Expand Down
33 changes: 33 additions & 0 deletions crates/cairo-compile-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "cairo-compile-utils"
version = "0.1.0"
edition = "2021"
description = "The source (Cairo) code of the dummy concat aggregator."

[features]
dump_source_files = []

[dependencies]
bincode.workspace = true
cairo-vm.workspace = true
cairo-lang-executable.workspace = true
cairo-lang-runner.workspace = true
cairo-lang-casm.workspace = true
cairo-lang-execute-utils.workspace = true
clap.workspace = true
num-traits.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
thiserror-no-std.workspace = true
regex.workspace = true
num-bigint.workspace = true
walkdir = "2"
anyhow = "1.0"

[build-dependencies]
serde_json.workspace = true

[dev-dependencies]
assert_matches = "1.5.0"
rstest = "0.19.0"
116 changes: 116 additions & 0 deletions crates/cairo-compile-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::process::Command;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use walkdir::WalkDir; // A useful crate for walking directories
use std::env; // Import the env module
use anyhow::{Result, Context};

use std::fs;

fn find_main_file(project_root: &PathBuf) -> anyhow::Result<PathBuf> {
for entry in WalkDir::new(project_root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_type().is_file() && e.path().extension().map_or(false, |ext| ext == "cairo"))
{
let content = fs::read_to_string(entry.path())?;

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
The application builds a file path from potentially untrusted data, which can lead to a path traversal vulnerability. An attacker can manipulate the path which the application uses to access files. If the application does not validate user input and sanitize file paths, sensitive files such as configuration or user data can be accessed, potentially creating or overwriting files. To prevent this vulnerability, validate and sanitize any input that is used to create references to file paths. Also, enforce strict file access controls. For example, choose privileges allowing public-facing applications to access only the required files.

Dataflow graph
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>crates/cairo-compile-utils/src/lib.rs</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0["<a href=https://github.com/starkware-libs/bootloader-hints/blob/98d341a3e5ffab9a225e8524f4f31e02d54e4b3f/crates/cairo-compile-utils/src/lib.rs#L16 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 16] entry.path()</a>"]
        end
        %% Intermediate

        %% Sink

        subgraph Sink
            direction LR

            v1["<a href=https://github.com/starkware-libs/bootloader-hints/blob/98d341a3e5ffab9a225e8524f4f31e02d54e4b3f/crates/cairo-compile-utils/src/lib.rs#L16 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 16] entry.path()</a>"]
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    File0:::invis

    %% Connections

    Source --> Sink


Loading

To resolve this comment:

✨ Commit Assistant fix suggestion

Suggested change
let content = fs::read_to_string(entry.path())?;
// Ensure the entry's path is within the intended directory to prevent path traversal
if !entry.path().starts_with(project_root) {
continue; // Skip files outside the target directory
}
let content = fs::read_to_string(entry.path())?;
View step-by-step instructions
  1. Validate that entry.path() only points to files within the intended directory (project_root) by checking that the entry's path starts with project_root. You can do this with entry.path().starts_with(project_root).
  2. Before calling fs::read_to_string(entry.path()), add a check:
    if !entry.path().starts_with(project_root) { continue; }
  3. Alternatively, if you expect only .cairo files in a fixed folder, consider filtering out any files with path components like .. or symlinks that could point outside the target directory, to prevent path traversal.
  4. Reject or skip any files not meeting the above requirements before processing their contents.

This will make sure your code only reads files in your intended directory and avoids unwanted file access.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by tainted-path.

You can view more details about this finding in the Semgrep AppSec Platform.

if content.contains("func main") {
return Ok(entry.path().to_path_buf());
}
}

Err(anyhow::anyhow!(
"No Cairo 0 file with `func main` found under {:?}",
project_root
))
}

/// Builds a shell-like string representation of a Command for logging/debugging.
/// This is a simplified representation and might not handle all edge cases (e.g., complex quoting).
fn format_command_for_display(cmd: &Command) -> String {
let mut cmd_str = String::new();

// Add the program name
if let Some(program) = cmd.get_program().to_str() {
cmd_str.push_str(program);
}

// Add arguments
for arg in cmd.get_args() {
if let Some(s) = arg.to_str() {
cmd_str.push(' ');
// Basic quoting for arguments that might contain spaces
if s.contains(' ') || s.contains('"') || s.contains('\'') {
cmd_str.push('"');
cmd_str.push_str(&s.replace('"', "\\\"")); // Escape existing double quotes
cmd_str.push('"');
} else {
cmd_str.push_str(s);
}
}
}

cmd_str
}

// current_dir should hold the relevant cairo code (only one main)
pub fn compile_cairo_code(current_dir: &PathBuf) -> anyhow::Result<()> {

println!("Current working directory: {:?}", current_dir);

// Define the output path for the compiled program (CASM)
let output_casm_file = PathBuf::from("./target/my_combined_program.json");

// Ensure the target directory exists
std::fs::create_dir_all(output_casm_file.parent().unwrap())?;

println!("Searching for Cairo 0 files in: {:?}", current_dir);

// Define the main Cairo file to compile
let main_cairo_file = find_main_file(&current_dir)?;
println!("Detected main Cairo file: {:?}", main_cairo_file);

// Check that it exists
if !main_cairo_file.exists() {
return Err(anyhow::anyhow!("Main Cairo file not found: {:?}", main_cairo_file));
}

println!("Attempting to compile Cairo 0 project to: {:?}", output_casm_file);

// Build the `cairo-compile` command
let mut command = Command::new("cairo-compile");

// Add only the main file
command.arg(&main_cairo_file);

println!("Executing the following command: {}", format_command_for_display(&command));
// Specify the output file
command.arg("--output").arg(&output_casm_file);

let cairo_project_root = current_dir.clone();
// Add the --cairo_path argument using the determined project root
// This is crucial for resolving 'from starkware.cairo...' imports if 'starkware'
// is a top-level directory directly under your project root.
command.arg("--cairo_path").arg(&cairo_project_root);

// Execute the command
let output = command.output()?;

// Check if the command was successful
if output.status.success() {
println!("Cairo 0 compilation successful!");
println!("Compiled program written to: {:?}", output_casm_file);
io::stdout().write_all(&output.stdout)?;
} else {
eprintln!("Cairo 0 compilation failed!");
eprintln!("Status: {:?}", output.status);
eprintln!("Stderr:\n{}", String::from_utf8_lossy(&output.stderr));
return Err(anyhow::anyhow!("Cairo 0 compilation failed: {}", String::from_utf8_lossy(&output.stderr)));
}

println!("\nTo run the compiled Cairo 0 program, you would typically use `cairo-run`:");
println!("`cairo-run --program {}`", output_casm_file.display());
println!("If `main` is an external function for a StarkNet contract, you'd deploy it.");

Ok(())
}
34 changes: 34 additions & 0 deletions crates/concat-aggregator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "concat-aggregator"
version = "0.1.0"
edition = "2021"
description = "The source (Cairo) code of the dummy concat aggregator."

[features]
dump_source_files = []

[dependencies]
bincode.workspace = true
cairo-vm.workspace = true
cairo-lang-executable.workspace = true
cairo-lang-runner.workspace = true
cairo-lang-casm.workspace = true
cairo-lang-execute-utils.workspace = true
clap.workspace = true
num-traits.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
thiserror.workspace = true
thiserror-no-std.workspace = true
regex.workspace = true
num-bigint.workspace = true
walkdir = "2"
anyhow = "1.0"
cairo-compile-utils = { path = "../cairo-compile-utils"}

[build-dependencies]
serde_json.workspace = true

[dev-dependencies]
assert_matches = "1.5.0"
rstest = "0.19.0"
Empty file.
60 changes: 60 additions & 0 deletions crates/concat-aggregator/src/aggregator_tasks_utils.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Parses the task outputs from the bootloader output. Writes their outputs to the output_ptr.
// Returns the number of tasks.
func parse_tasks{output_ptr: felt*}() -> felt {
alloc_locals;

local n_tasks: felt;

%{
def parse_bootloader_tasks_outputs(output):
"""
Parses the output of the bootloader, returning the raw outputs of the tasks.
"""
output_iter = iter(output)
# Skip the bootloader_config.
[next(output_iter) for _ in range(3)]

n_tasks = next(output_iter)
tasks_outputs = []
for _ in range(n_tasks):
task_output_size = next(output_iter)
tasks_outputs.append([next(output_iter) for _ in range(task_output_size - 1)])

assert next(output_iter, None) is None, "Bootloader output wasn't fully consumed."

return tasks_outputs

tasks_outputs = parse_bootloader_tasks_outputs(program_input["bootloader_output"])
assert len(tasks_outputs) > 0, "No tasks found in the bootloader output."
ids.n_tasks = len(tasks_outputs)
%}

assert [output_ptr] = n_tasks;
let output_ptr = output_ptr + 1;

// Output the task outputs as they are.
output_tasks(n_tasks=n_tasks);

return n_tasks;
}

// Outputs the task outputs, each with the size of the output (to match the bootloader output
// format).
func output_tasks{output_ptr: felt*}(n_tasks: felt) {
if (n_tasks == 0) {
return ();
}

let output_size = output_ptr[0];
let output_ptr = output_ptr + 1;

%{
task_index = len(tasks_outputs) - ids.n_tasks
segments.load_data(ptr=ids.output_ptr, data=tasks_outputs[task_index])
ids.output_size = len(tasks_outputs[task_index]) + 1
%}

let output_ptr = output_ptr + output_size - 1;

return output_tasks(n_tasks=n_tasks - 1);
}
69 changes: 69 additions & 0 deletions crates/concat-aggregator/src/concat_aggregator.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
%builtins output range_check poseidon

from aggregator_tasks_utils import parse_tasks
from starkware.cairo.common.cairo_builtins import PoseidonBuiltin

// Simple aggregation program that concatenates task outputs. Used for tests. It is not sound.
//
// Hint arguments:
// program_input - List of task outputs, in the format of the bootloader output.
func main{output_ptr: felt*, range_check_ptr, poseidon_ptr: PoseidonBuiltin*}() {
alloc_locals;

let n_tasks = parse_tasks();
local output_start: felt* = output_ptr;

// Output the concatenated task outputs.
output_concatenated_output(n_tasks=n_tasks);

%{
from starkware.python.math_utils import div_ceil

output_length = ids.output_ptr - ids.output_start
page_size = 10
next_page_start = min(ids.output_start + page_size, ids.output_ptr)
next_page_id = 1
while next_page_start < ids.output_ptr:
output_builtin.add_page(
page_id=next_page_id,
page_start=next_page_start,
page_size=min(ids.output_ptr - next_page_start, page_size),
)
next_page_start += page_size
next_page_id += 1
if next_page_id == 1:
# Single page. Use trivial fact topology.
output_builtin.add_attribute('gps_fact_topology', [
1,
0,
])
else:
output_builtin.add_attribute('gps_fact_topology', [
next_page_id,
next_page_id - 1,
0,
2,
])
%}
return ();
}

// Outputs the task outputs, without the output sizes.
func output_concatenated_output{output_ptr: felt*}(n_tasks: felt) {
alloc_locals;
if (n_tasks == 0) {
return ();
}

local output_size: felt;

%{
task_index = len(tasks_outputs) - ids.n_tasks
segments.load_data(ptr=ids.output_ptr, data=tasks_outputs[task_index])
ids.output_size = len(tasks_outputs[task_index])
%}

let output_ptr = output_ptr + output_size;

return output_concatenated_output(n_tasks=n_tasks - 1);
}
20 changes: 20 additions & 0 deletions crates/concat-aggregator/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::process::Command;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use walkdir::WalkDir; // A useful crate for walking directories
use std::env; // Import the env module
use anyhow::{Result, Context};

use cairo_compile_utils::compile_cairo_code;
use std::fs;

fn main() -> anyhow::Result<()> {

// Get the current working directory of the Rust program
let current_dir = env::current_dir()
.context("Failed to get current working directory")?.join("crates").join("concat-aggregator").join("src");

compile_cairo_code(&current_dir)?;

Ok(())
}
Loading