From a29792517acca33faaa1fb08be8e46c6827129f3 Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 28 Jul 2026 10:55:07 +0800 Subject: [PATCH 1/2] fix: harden compiler and regression inputs --- libsolidity/interface/StandardCompiler.cpp | 2 ++ scripts/regressions.py | 29 +++++++++++++++---- scripts/splitSources.py | 17 +++++++++-- .../input.json | 13 +++++++++ .../output.json | 11 +++++++ 5 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 test/cmdlineTests/standard_wrong_type_bytecodeHash/input.json create mode 100644 test/cmdlineTests/standard_wrong_type_bytecodeHash/output.json diff --git a/libsolidity/interface/StandardCompiler.cpp b/libsolidity/interface/StandardCompiler.cpp index 85495a9e37e2..fbb1a4c99868 100644 --- a/libsolidity/interface/StandardCompiler.cpp +++ b/libsolidity/interface/StandardCompiler.cpp @@ -513,6 +513,8 @@ std::optional checkMetadataKeys(Json const& _input) return formatFatalError(Error::Type::JSONError, "\"settings.metadata.appendCBOR\" must be Boolean"); if (_input.contains("useLiteralContent") && !_input["useLiteralContent"].is_boolean()) return formatFatalError(Error::Type::JSONError, "\"settings.metadata.useLiteralContent\" must be Boolean"); + if (_input.contains("bytecodeHash") && !_input["bytecodeHash"].is_string()) + return formatFatalError(Error::Type::JSONError, "\"settings.metadata.bytecodeHash\" must be a string"); static std::set hashes{"ipfs", "bzzr1", "none"}; if (_input.contains("bytecodeHash") && !hashes.count(_input["bytecodeHash"].get())) diff --git a/scripts/regressions.py b/scripts/regressions.py index e30c0aeb9f8d..8206b359c018 100755 --- a/scripts/regressions.py +++ b/scripts/regressions.py @@ -52,7 +52,7 @@ def parseCmdLine(cls, description, args): def run_cmd(command, logfile=None, env=None): """ Args: - command (str): command to run + command (list[str]): command and arguments to run logfile (str): log file name env (dict): dictionary holding key-value pairs for bash environment variables @@ -69,13 +69,33 @@ def run_cmd(command, logfile=None, env=None): env = os.environ.copy() with open(logfile, 'w', encoding='utf8') as logfh: - with subprocess.Popen(command, shell=True, executable='/bin/bash', - env=env, stdout=logfh, + with subprocess.Popen(command, env=env, stdout=logfh, stderr=subprocess.STDOUT) as proc: ret = proc.wait() logfh.close() return ret + @staticmethod + def run_corpus(fuzzer, corpus_dir, logfile, env=None): + if not env: + env = os.environ.copy() + + corpus_files = [] + for root, _, filenames in os.walk(corpus_dir): + corpus_files.extend(os.path.join(root, filename) for filename in filenames) + + with open(logfile, 'w', encoding='utf8') as logfh: + for corpus_file in sorted(corpus_files): + with subprocess.Popen( + [fuzzer, corpus_file], + env=env, + stdout=logfh, + stderr=subprocess.STDOUT + ) as proc: + if proc.wait() != 0: + return 255 + return 0 + def process_log(self, logfile): """ Args: @@ -106,8 +126,7 @@ def run(self): basename = os.path.basename(fuzzer) logfile = os.path.join(self._logpath, f"{basename}.log") corpus_dir = f"/tmp/solidity-fuzzing-corpus/{basename}_seed_corpus" - cmd = f"find {corpus_dir} -type f | xargs -n1 sh -c '{fuzzer} $0 || exit 255'" - self.run_cmd(cmd, logfile=logfile) + self.run_corpus(fuzzer, corpus_dir, logfile) ret = self.process_log(logfile) if not ret: print( diff --git a/scripts/splitSources.py b/scripts/splitSources.py index 5e783b2d5aa9..8607ee2d9cbf 100755 --- a/scripts/splitSources.py +++ b/scripts/splitSources.py @@ -10,8 +10,8 @@ # - 'false' if the file only had one source import sys -import os import traceback +from pathlib import Path def uncaught_exception_hook(exc_type, exc_value, exc_traceback): @@ -37,9 +37,20 @@ def writeSourceToFile(lines): filePath, srcName = extractSourceName(lines[0]) # print("sourceName is ", srcName) # print("filePath is", filePath) + outputRoot = Path.cwd().resolve() + sourcePath = Path(srcName) + if sourcePath.is_absolute(): + raise ValueError("Source name must be a relative path: " + srcName) + + outputPath = (outputRoot / sourcePath).resolve() + try: + outputPath.relative_to(outputRoot) + except ValueError: + raise ValueError("Source name escapes the output directory: " + srcName) + if filePath: - os.system("mkdir -p " + filePath) - with open(srcName, mode='a+', encoding='utf8', newline='') as f: + outputPath.parent.mkdir(parents=True, exist_ok=True) + with outputPath.open(mode='a+', encoding='utf8', newline='') as f: for idx, line in enumerate(lines[1:]): # write to file if line[:12] != "==== Source:": diff --git a/test/cmdlineTests/standard_wrong_type_bytecodeHash/input.json b/test/cmdlineTests/standard_wrong_type_bytecodeHash/input.json new file mode 100644 index 000000000000..565a94738c0a --- /dev/null +++ b/test/cmdlineTests/standard_wrong_type_bytecodeHash/input.json @@ -0,0 +1,13 @@ +{ + "language": "Solidity", + "sources": { + "A.sol": { + "content": "pragma solidity >=0.0; contract A {}" + } + }, + "settings": { + "metadata": { + "bytecodeHash": true + } + } +} diff --git a/test/cmdlineTests/standard_wrong_type_bytecodeHash/output.json b/test/cmdlineTests/standard_wrong_type_bytecodeHash/output.json new file mode 100644 index 000000000000..114c86e30f92 --- /dev/null +++ b/test/cmdlineTests/standard_wrong_type_bytecodeHash/output.json @@ -0,0 +1,11 @@ +{ + "errors": [ + { + "component": "general", + "formattedMessage": "\"settings.metadata.bytecodeHash\" must be a string", + "message": "\"settings.metadata.bytecodeHash\" must be a string", + "severity": "error", + "type": "JSONError" + } + ] +} From 8072fcfd264d61a8c9c3958af5e37d85b8c93b47 Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 28 Jul 2026 17:19:00 +0800 Subject: [PATCH 2/2] fix(lsp): avoid scanning the filesystem root --- libsolidity/lsp/LanguageServer.cpp | 18 ++++++++++++++++-- test/lsp.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp index d726139e3a73..d156b66422ac 100644 --- a/libsolidity/lsp/LanguageServer.cpp +++ b/libsolidity/lsp/LanguageServer.cpp @@ -59,6 +59,12 @@ namespace fs = boost::filesystem; namespace { +bool isFilesystemRoot(fs::path const& _path) +{ + auto const normalizedPath = _path.lexically_normal(); + return normalizedPath.has_root_path() && normalizedPath == normalizedPath.root_path(); +} + bool resolvesToRegularFile(boost::filesystem::path _path, int maxRecursionDepth = 10) { fs::file_status fileStatus = fs::status(_path); @@ -217,14 +223,20 @@ void LanguageServer::changeConfiguration(Json const& _settings) std::vector LanguageServer::allSolidityFilesFromProject() const { std::vector collectedPaths{}; + auto const basePath = m_fileRepository.basePath().lexically_normal(); + + // The filesystem root is used as a fallback when the client does not provide a workspace root. + // Keep full filesystem access available for explicit imports, but never scan the entire filesystem eagerly. + if (isFilesystemRoot(basePath)) + return collectedPaths; // We explicitly decided against including all files from include paths but leave the possibility // open for a future PR to enable such a feature to be optionally enabled (default disabled). // Note: Newer versions of boost have deprecated symlink_option::recurse #if (BOOST_VERSION < 107200) - auto directoryIterator = fs::recursive_directory_iterator(m_fileRepository.basePath(), fs::symlink_option::recurse); + auto directoryIterator = fs::recursive_directory_iterator(basePath, fs::symlink_option::recurse); #else - auto directoryIterator = fs::recursive_directory_iterator(m_fileRepository.basePath(), fs::directory_options::follow_directory_symlink); + auto directoryIterator = fs::recursive_directory_iterator(basePath, fs::directory_options::follow_directory_symlink); #endif for (fs::directory_entry const& dirEntry: directoryIterator) if ( @@ -412,6 +424,8 @@ void LanguageServer::handleInitialize(MessageID _id, Json const& _args) m_fileRepository = FileRepository(rootPath, {}); if (_args.contains("initializationOptions") && _args["initializationOptions"].is_object()) changeConfiguration(_args["initializationOptions"]); + if (m_fileLoadStrategy == FileLoadStrategy::ProjectDirectory && isFilesystemRoot(m_fileRepository.basePath())) + m_fileLoadStrategy = FileLoadStrategy::DirectlyOpenedAndOnImported; Json replyArgs; replyArgs["serverInfo"]["name"] = "solc"; diff --git a/test/lsp.py b/test/lsp.py index 11007ae822ec..16da8a25b1ac 100755 --- a/test/lsp.py +++ b/test/lsp.py @@ -1330,6 +1330,22 @@ def user_interaction_failed_autoupdate(self, test, sub_dir): # }}} # {{{ actual tests + def test_project_directory_without_workspace_root(self, solc: JsonRpcProcess) -> None: + """ + A missing workspace root falls back to the filesystem root internally. Project-directory + loading must not interpret that fallback as a request to scan the entire filesystem. + """ + self.setup_lsp( + solc, + expose_project_root=False, + file_load_strategy=FileLoadStrategy.ProjectDirectory + ) + TEST_NAME = 'publish_diagnostics_3' + published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) + + self.expect_equal(len(published_diagnostics), 1, "Only the directly opened file is analyzed") + self.expect_equal(published_diagnostics[0]['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") + def test_analyze_all_project_files_flat(self, solc: JsonRpcProcess) -> None: """ Tests the option (default) to analyze all .sol project files even when they have not been actively