From 2dee9ac7a525d9fb3d5196602ca6f951ead0c26d Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:45:51 +0000 Subject: [PATCH] Fix other in crates/dynamic/src/lib.rs --- crates/cli/src/config.rs | 17 +++++++++-- crates/cli/src/lang/custom_lang.rs | 28 +++++++++++++++--- crates/cli/src/lang/mod.rs | 8 ++++-- crates/cli/src/lib.rs | 46 ++++++++++++++---------------- crates/dynamic/src/lib.rs | 36 +++++++++++++++++++++-- crates/pyo3/src/py_lang.rs | 2 +- 6 files changed, 100 insertions(+), 37 deletions(-) diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 48a15bf7c6..ac4e429235 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -91,6 +91,13 @@ impl ProjectConfig { } // do not report error if no sgconfig.yml is found pub fn setup(config_path: Option) -> Result> { + Self::setup_with_native_plugins(config_path, false) + } + + pub fn setup_with_native_plugins( + config_path: Option, + allow_native_plugins: bool, + ) -> Result> { let Some((project_dir, mut sg_config)) = Self::discover_project(config_path)? else { return Ok(Err(anyhow::anyhow!(EC::ProjectNotExist))); }; @@ -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)?; diff --git a/crates/cli/src/lang/custom_lang.rs b/crates/cli/src/lang/custom_lang.rs index 172afa4aa9..80adad0c7c 100644 --- a/crates/cli/src/lang/custom_lang.rs +++ b/crates/cli/src/lang/custom_lang.rs @@ -1,4 +1,5 @@ use ast_grep_dynamic::{DynamicLang, Registration}; +use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -16,13 +17,32 @@ pub struct CustomLang { } impl CustomLang { - pub fn register(base: &Path, langs: HashMap) { + pub fn register( + base: &Path, + langs: HashMap, + 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::>(); + if allow_native_plugins { + for registration in ®istrations { + 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(()) } } diff --git a/crates/cli/src/lang/mod.rs b/crates/cli/src/lang/mod.rs index 794abf46bc..4f280d77a6 100644 --- a/crates/cli/src/lang/mod.rs +++ b/crates/cli/src/lang/mod.rs @@ -40,8 +40,12 @@ impl SgLang { } // register_globs must be called after register_custom_language - pub fn register_custom_language(base: &Path, langs: HashMap) { - CustomLang::register(base, langs) + pub fn register_custom_language( + base: &Path, + langs: HashMap, + allow_native_plugins: bool, + ) -> Result<()> { + CustomLang::register(base, langs, allow_native_plugins) } // TODO: add tests diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index cfbdfe3dc1..9ec1b39c5c 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -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, + /// Trust and load native custom-language plugins from project configuration. + #[clap(long, global = true)] + allow_native_plugins: bool, } #[derive(Subcommand)] @@ -84,38 +87,32 @@ fn try_default_run(args: &[String]) -> Result> { } } -/// finding project and setup custom language configuration -fn setup_project_is_possible(args: &[String]) -> Result { - 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) -> 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::(arg), Commands::Docs => todo!("todo, generate rule docs based on current config"), } @@ -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"); diff --git a/crates/dynamic/src/lib.rs b/crates/dynamic/src/lib.rs index 2fe4982438..40176c9d93 100644 --- a/crates/dynamic/src/lib.rs +++ b/crates/dynamic/src/lib.rs @@ -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")] @@ -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) -> 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) -> Result<(), DynamicLangError> { debug_assert!(Self::langs().is_empty()); let mut langs = vec![]; let mut mapping = vec![]; @@ -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 { @@ -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); diff --git a/crates/pyo3/src/py_lang.rs b/crates/pyo3/src/py_lang.rs index 4623bc5027..c6ad8d1847 100644 --- a/crates/pyo3/src/py_lang.rs +++ b/crates/pyo3/src/py_lang.rs @@ -37,7 +37,7 @@ fn register(base: PathBuf, langs: HashMap) { .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 {