Skip to content
Merged
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
68 changes: 63 additions & 5 deletions obli-transpiler-framework/backend/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,57 @@ use crate::error::Error;
use crate::oir::*;
use std::fmt::Write;

/// Validate that an identifier is safe for code generation
///
/// Returns an error if the identifier contains characters that could
/// enable code injection attacks.
fn validate_identifier(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::codegen("empty identifier"));
}

// First character must be letter or underscore
let first = name.chars().next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' {
return Err(Error::codegen(format!(
"invalid identifier '{}': must start with letter or underscore",
name
)));
}

// Rest must be alphanumeric or underscore
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '_' {
return Err(Error::codegen(format!(
"invalid identifier '{}': contains forbidden character '{}'",
name, c
)));
}
}

// Check for Rust reserved keywords that could cause issues
const DANGEROUS_NAMES: &[&str] = &[
"unsafe", "asm", "extern", "mod", "crate", "self", "super",
"macro_rules", "include", "include_str", "include_bytes",
];
if DANGEROUS_NAMES.contains(&name) {
return Err(Error::codegen(format!(
"identifier '{}' is reserved and cannot be used",
name
)));
}

Ok(())
}

/// Sanitize an identifier for safe code generation
///
/// Validates and returns the identifier, or returns an error.
fn sanitize_ident(name: &str) -> Result<&str, Error> {
validate_identifier(name)?;
Ok(name)
}

/// Code generator state
pub struct CodeGenerator {
indent: usize,
Expand Down Expand Up @@ -264,7 +315,10 @@ impl CodeGenerator {
match expr {
Expr::Lit(lit) => self.emit_literal(lit)?,

Expr::Var(name) => write!(self.output, "{}", name)?,
Expr::Var(name) => {
let safe_name = sanitize_ident(name)?;
write!(self.output, "{}", safe_name)?;
}

Expr::Binop(op, lhs, rhs) => {
write!(self.output, "(")?;
Expand All @@ -280,7 +334,8 @@ impl CodeGenerator {
}

Expr::Call(name, args) => {
write!(self.output, "{}(", name)?;
let safe_name = sanitize_ident(name)?;
write!(self.output, "{}(", safe_name)?;
for (i, arg) in args.iter().enumerate() {
if i > 0 {
write!(self.output, ", ")?;
Expand All @@ -298,8 +353,9 @@ impl CodeGenerator {
}

Expr::Field(obj, field) => {
let safe_field = sanitize_ident(field)?;
self.emit_expr(obj)?;
write!(self.output, ".{}", field)?;
write!(self.output, ".{}", safe_field)?;
}

Expr::Cmov(cond, then_val, else_val) => {
Expand All @@ -324,12 +380,14 @@ impl CodeGenerator {
}

Expr::Struct(name, fields) => {
write!(self.output, "{} {{", name)?;
let safe_name = sanitize_ident(name)?;
write!(self.output, "{} {{", safe_name)?;
for (i, (fname, fval)) in fields.iter().enumerate() {
let safe_fname = sanitize_ident(fname)?;
if i > 0 {
write!(self.output, ",")?;
}
write!(self.output, " {}: ", fname)?;
write!(self.output, " {}: ", safe_fname)?;
self.emit_expr(fval)?;
}
write!(self.output, " }}")?;
Expand Down
80 changes: 73 additions & 7 deletions obli-transpiler-framework/driver/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,74 @@
//! Compilation pipeline implementation

use crate::error::Error;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;

/// Validate that a path is safe for use (no path traversal)
///
/// Ensures the path doesn't contain ".." or other traversal attempts.
fn validate_path(path: &Path) -> Result<(), Error> {
// Check for path traversal attempts
for component in path.components() {
if let std::path::Component::ParentDir = component {
return Err(Error::InvalidInput(format!(
"path '{}' contains parent directory reference (..)",
path.display()
)));
}
}

// Check the path string for other dangerous patterns
let path_str = path.to_string_lossy();
if path_str.contains('\0') {
return Err(Error::InvalidInput("path contains null byte".to_string()));
}

Ok(())
}

/// Validate and normalize an input file path
fn validate_input_path(path: &Path) -> Result<PathBuf, Error> {
validate_path(path)?;

// Verify the file exists
if !path.exists() {
return Err(Error::InputNotFound(path.display().to_string()));
}

// Use canonicalize to resolve to absolute path
path.canonicalize()
.map_err(|e| Error::InvalidInput(format!("cannot resolve path '{}': {}", path.display(), e)))
}

/// Validate an output path (doesn't need to exist, but must be safe)
fn validate_output_path(path: &Path) -> Result<PathBuf, Error> {
validate_path(path)?;

// If parent exists, canonicalize parent and join filename
if let Some(parent) = path.parent() {
if parent.as_os_str().is_empty() {
// No parent means current directory - that's fine
Ok(path.to_path_buf())
} else if parent.exists() {
let canonical_parent = parent.canonicalize()
.map_err(|e| Error::InvalidInput(format!(
"cannot resolve parent directory '{}': {}",
parent.display(), e
)))?;
Ok(canonical_parent.join(path.file_name().unwrap_or_default()))
} else {
Err(Error::InvalidInput(format!(
"parent directory '{}' does not exist",
parent.display()
)))
}
} else {
Ok(path.to_path_buf())
}
}

/// Configuration for compile command
pub struct CompileConfig {
pub input: PathBuf,
Expand Down Expand Up @@ -80,16 +144,18 @@ fn find_backend() -> Result<PathBuf, Error> {

/// Compile .obl to .rs
pub fn compile(config: CompileConfig) -> Result<(), Error> {
if !config.input.exists() {
return Err(Error::InputNotFound(config.input.display().to_string()));
}
// Validate input path (security: prevent path traversal)
let input = validate_input_path(&config.input)?;

let frontend = find_frontend()?;
let backend = find_backend()?;

// Determine output paths
let oir_path = config.input.with_extension("oir.json");
let rs_path = config.output.unwrap_or_else(|| config.input.with_extension("rs"));
// Determine and validate output paths
let oir_path = validate_output_path(&input.with_extension("oir.json"))?;
let rs_path = match config.output {
Some(ref p) => validate_output_path(p)?,
None => validate_output_path(&input.with_extension("rs"))?,
};

if config.verbose {
eprintln!("Using frontend: {}", frontend.display());
Expand Down
9 changes: 8 additions & 1 deletion obli-transpiler-framework/frontend/lib/typecheck.ml
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,18 @@ and check_stmt state env stmt =
| SAssign (lhs, rhs) ->
let lhs_at = check_expr state env lhs in
let rhs_at = check_expr state env rhs in
(* Check type compatibility *)
if not (types_equal lhs_at.typ rhs_at.typ) then
report state.diags (type_mismatch
~expected:(type_to_string lhs_at.typ)
~found:(type_to_string rhs_at.typ)
rhs.expr_loc)
rhs.expr_loc);
(* CRITICAL: Check security label - cannot assign high to low (information leak) *)
if not (security_leq rhs_at.security lhs_at.security) then
report state.diags (information_leak
~from_label:(security_to_string rhs_at.security)
~to_label:(security_to_string lhs_at.security)
stmt.stmt_loc)

| SOramWrite (arr, idx, value) ->
let arr_at = check_expr state env arr in
Expand Down
Loading
Loading