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
17 changes: 14 additions & 3 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ impl ProjectConfig {
}
// do not report error if no sgconfig.yml is found
pub fn setup(config_path: Option<PathBuf>) -> Result<Result<Self>> {
Self::setup_with_native_plugins(config_path, false)
}

pub fn setup_with_native_plugins(
config_path: Option<PathBuf>,
allow_native_plugins: bool,
) -> Result<Result<Self>> {
let Some((project_dir, mut sg_config)) = Self::discover_project(config_path)? else {
return Ok(Err(anyhow::anyhow!(EC::ProjectNotExist)));
};
Expand All @@ -101,14 +108,18 @@ impl ProjectConfig {
util_dirs: sg_config.util_dirs.take(),
};
// sg_config will not use rule dirs and test configs anymore
register_custom_language(&config.project_dir, sg_config)?;
register_custom_language(&config.project_dir, sg_config, allow_native_plugins)?;
Ok(Ok(config))
}
}

fn register_custom_language(project_dir: &Path, sg_config: AstGrepConfig) -> Result<()> {
fn register_custom_language(
project_dir: &Path,
sg_config: AstGrepConfig,
allow_native_plugins: bool,
) -> Result<()> {
if let Some(custom_langs) = sg_config.custom_languages {
SgLang::register_custom_language(project_dir, custom_langs);
SgLang::register_custom_language(project_dir, custom_langs, allow_native_plugins)?;
}
if let Some(globs) = sg_config.language_globs {
SgLang::register_globs(globs)?;
Expand Down
28 changes: 24 additions & 4 deletions crates/cli/src/lang/custom_lang.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ast_grep_dynamic::{DynamicLang, Registration};
use anyhow::Result;
use serde::{Deserialize, Serialize};

use std::collections::HashMap;
Expand All @@ -16,13 +17,32 @@ pub struct CustomLang {
}

impl CustomLang {
pub fn register(base: &Path, langs: HashMap<String, CustomLang>) {
pub fn register(
base: &Path,
langs: HashMap<String, CustomLang>,
allow_native_plugins: bool,
) -> Result<()> {
let registrations = langs
.into_iter()
.map(|(name, custom)| to_registration(name, custom, base))
.collect();
// TODO, add error handling
unsafe { DynamicLang::register(registrations).expect("TODO") }
.collect::<Vec<_>>();
if allow_native_plugins {
for registration in &registrations {
eprintln!(
"Warning: loading native plugin `{}` (symbol `{}`).",
registration.lib_path.display(),
registration.symbol,
);
}
}
unsafe {
if allow_native_plugins {
DynamicLang::register_trusted(registrations)?;
} else {
DynamicLang::register(registrations)?;
}
}
Ok(())
}
}

Expand Down
8 changes: 6 additions & 2 deletions crates/cli/src/lang/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ impl SgLang {
}

// register_globs must be called after register_custom_language
pub fn register_custom_language(base: &Path, langs: HashMap<String, CustomLang>) {
CustomLang::register(base, langs)
pub fn register_custom_language(
base: &Path,
langs: HashMap<String, CustomLang>,
allow_native_plugins: bool,
) -> Result<()> {
CustomLang::register(base, langs, allow_native_plugins)
}

// TODO: add tests
Expand Down
46 changes: 22 additions & 24 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ struct App {
/// Path to ast-grep root config, default is sgconfig.yml.
#[clap(short, long, global = true, value_name = "CONFIG_FILE")]
config: Option<PathBuf>,
/// Trust and load native custom-language plugins from project configuration.
#[clap(long, global = true)]
allow_native_plugins: bool,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -84,38 +87,32 @@ fn try_default_run(args: &[String]) -> Result<Option<RunArg>> {
}
}

/// finding project and setup custom language configuration
fn setup_project_is_possible(args: &[String]) -> Result<ProjectConfig> {
let mut config = None;
for i in 0..args.len() {
if args[i] != "-c" && args[i] != "--config" {
continue;
}
if i + 1 >= args.len() || args[i + 1].starts_with('-') {
return Err(anyhow::anyhow!("missing config file after -c"));
}
let config_file = (&args[i + 1]).into();
config = Some(config_file);
}
ProjectConfig::setup(config)?
}

// this wrapper function is for testing
pub fn main_with_args(args: impl Iterator<Item = String>) -> Result<()> {
let args: Vec<_> = args.collect();
let project = setup_project_is_possible(&args);
// register_custom_language_if_is_run(&args)?;
if let Some(arg) = try_default_run(&args)? {
return run_with_pattern(arg);
}

let app = App::try_parse_from(args)?;
match app.command {
Commands::Run(arg) => run_with_pattern(arg),
Commands::Scan(arg) => run_with_config(arg, project),
Commands::Test(arg) => run_test_rule(arg, project),
Commands::New(arg) => run_create_new(arg, project),
Commands::Lsp(arg) => run_language_server(arg, project),
let App {
command,
config,
allow_native_plugins,
} = app;
let has_explicit_config = config.is_some();
let setup_project = || ProjectConfig::setup_with_native_plugins(config, allow_native_plugins)?;
match command {
Commands::Run(arg) => {
if has_explicit_config {
setup_project()?;
}
run_with_pattern(arg)
}
Commands::Scan(arg) => run_with_config(arg, setup_project()),
Commands::Test(arg) => run_test_rule(arg, setup_project()),
Commands::New(arg) => run_create_new(arg, setup_project()),
Commands::Lsp(arg) => run_language_server(arg, setup_project()),
Commands::Completions(arg) => run_shell_completion::<App>(arg),
Commands::Docs => todo!("todo, generate rule docs based on current config"),
}
Expand Down Expand Up @@ -223,6 +220,7 @@ mod test_cli {
#[test]
fn test_scan() {
ok("scan");
ok("scan --allow-native-plugins");
ok("scan dir");
ok("scan -r test-rule.yml dir");
ok("scan -c test-rule.yml dir");
Expand Down
36 changes: 33 additions & 3 deletions crates/dynamic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ struct Inner {

#[derive(Debug, Error)]
pub enum DynamicLangError {
#[error("native plugin {path:?} (symbol `{symbol}`) requires explicit opt-in")]
NativePluginNotAllowed { path: PathBuf, symbol: String },
#[error("cannot load lib")]
OpenLib(#[source] LibError),
#[error("cannot read symbol")]
Expand Down Expand Up @@ -148,9 +150,23 @@ pub struct Registration {

impl DynamicLang {
/// # Safety
/// the register function should be called exactly once before use.
/// It relies on a global mut static variable to be initialized.
/// Native registration is disabled by default. Use `register_trusted` only
/// after an explicit trust decision.
pub unsafe fn register(regs: Vec<Registration>) -> Result<(), DynamicLangError> {
if let Some(reg) = regs.first() {
return Err(DynamicLangError::NativePluginNotAllowed {
path: reg.lib_path.clone(),
symbol: reg.symbol.clone(),
});
}
Ok(())
}

/// # Safety
/// The caller must trust every native library in `regs` and understand that
/// loading it executes code in the current process. This function must be
/// called exactly once before the languages are used.
pub unsafe fn register_trusted(regs: Vec<Registration>) -> Result<(), DynamicLangError> {
debug_assert!(Self::langs().is_empty());
let mut langs = vec![];
let mut mapping = vec![];
Expand Down Expand Up @@ -276,6 +292,20 @@ mod test {
);
}

#[test]
fn test_native_plugin_requires_opt_in() {
let registration = Registration {
lib_path: PathBuf::from("untrusted.so"),
symbol: "tree_sitter_untrusted".into(),
..Default::default()
};
let result = unsafe { DynamicLang::register(vec![registration]) };
let error = result.expect_err("native plugins must be denied by default");
let message = error.to_string();
assert!(message.contains("untrusted.so"));
assert!(message.contains("tree_sitter_untrusted"));
}

#[test]
fn test_register_lang() {
let registration = Registration {
Expand All @@ -287,7 +317,7 @@ mod test {
symbol: "tree_sitter_json".into(),
};
unsafe {
DynamicLang::register(vec![registration]).expect("should succeed");
DynamicLang::register_trusted(vec![registration]).expect("should succeed");
}
let langs = DynamicLang::all_langs();
assert_eq!(langs.len(), 1);
Expand Down
2 changes: 1 addition & 1 deletion crates/pyo3/src/py_lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn register(base: PathBuf, langs: HashMap<String, CustomLang>) {
.map(|(name, custom)| to_registration(name, custom, &base))
.collect();
// TODO, add error handling
unsafe { DynamicLang::register(registrations).expect("TODO") }
unsafe { DynamicLang::register_trusted(registrations).expect("TODO") }
}

fn to_registration(name: String, custom_lang: CustomLang, base: &Path) -> Registration {
Expand Down
Loading