diff --git a/.github/labeler.yml b/.github/labeler.yml index 65f820799716..380a4a8ed32c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -18,6 +18,9 @@ JS: - any: [ 'javascript/**/*' ] - change-notes/**/*javascript* +Lua: + - lua/**/* + Kotlin: - java/kotlin-extractor/**/* - java/ql/test-kotlin*/**/* diff --git a/.github/workflows/validate-change-notes.yml b/.github/workflows/validate-change-notes.yml index 6812d8ff0f6d..873dbf8f529c 100644 --- a/.github/workflows/validate-change-notes.yml +++ b/.github/workflows/validate-change-notes.yml @@ -31,4 +31,4 @@ jobs: - name: Fail if there are any errors with existing change notes run: | - codeql pack release --groups actions,cpp,csharp,go,java,javascript,python,ruby,shared,swift -examples,-test,-experimental + codeql pack release --groups actions,cpp,csharp,go,java,javascript,lua,python,ruby,shared,swift -examples,-test,-experimental diff --git a/lua/README.md b/lua/README.md new file mode 100644 index 000000000000..2727f2ec7ce4 --- /dev/null +++ b/lua/README.md @@ -0,0 +1,206 @@ +# Lua 5.1 language analysis for CodeQL + +This directory adds Lua 5.1 as a new language analysis capability to CodeQL. +It provides a self-contained extractor, database schema, QL libraries, query +packs, security queries, tests, and documentation for compiled Lua 5.1 +bytecode. Existing `.luac` files can be analyzed directly. Lua source is +analyzed by compiling it with `luac5.1` while preserving the source-tree +layout, then analyzing the generated bytecode. + +Plain `.lua` files are recorded as source inventory. Semantic, data-flow, and +taint facts come from `.luac` files; this implementation does not claim a Lua +source AST. + +## Capability matrix + +| Capability | Support | +| --- | --- | +| Language version | Lua 5.1 bytecode. Other bytecode versions are rejected with diagnostics. | +| Source workflow | Compile `.lua` files with `luac5.1`, preserving relative paths, then analyze the resulting `.luac` tree. | +| Bytecode input | Direct recursive ingestion of `.luac` files. | +| Bytecode model | Artifact profiles, prototypes, instructions, constants, closures, upvalues, register events, and call sites. | +| Local semantics | Control flow, dominance, reaching definitions, def-use, globals, upvalues, tables, and overwrite behavior. | +| Calls and modules | Local call targets, literal `require`, module resolution, returned exports, and module-field calls. | +| Interprocedural flow | Arguments, varargs, returns, open results, tail calls, and callsite-balanced summaries. | +| Taint flow | Local and cross-module source-to-sink paths over the bytecode value graph. | +| Sources and sinks | Resolved API-name models for external inputs and command-execution calls. | +| Sanitizers | Resolved API-name models with path-local and value-local classification. | +| Security output | `lua/command-injection` path findings in the experimental Lua security suite. | +| Review output | Sanitized paths and detailed bytecode/model facts as directly runnable diagnostic queries. | +| Diagnostics | Explicit rejection facts for malformed chunks and unsupported Lua versions or profiles. | +| Result formats | BQRS, CSV or JSON decoding, and SARIF for path queries. | + +## Prerequisites + +- A CodeQL CLI distribution. +- This repository checkout, used as the Lua extractor and pack search root. +- `luac5.1` only when starting from Lua source. + +The public repository contains the extractor and packs. Until Lua is included +in an official CodeQL CLI bundle, commands must include this checkout in +`--search-path`. CodeQL Action and official bundle registration are separate +upstream product integration work. + +The examples below run from the repository root: + +```bash +CODEQL=/absolute/path/to/codeql +ROOT=$(pwd) +``` + +## Analyze existing bytecode + +Set `INPUT` to an absolute directory containing `.luac` files. The directory +hierarchy below `INPUT` becomes the stable module layout in the database. + +```bash +INPUT=/absolute/path/to/lua-bytecode +DB=/tmp/codeql-lua51-db + +env -u PYTHONPATH CODEQL_LUA_BYTECODE_INPUT_ROOT="$INPUT" \ + "$CODEQL" database create "$DB" \ + --overwrite \ + --language=lua \ + --source-root="$INPUT" \ + --build-mode=none \ + --search-path="$ROOT:$ROOT/lua" +``` + +Run the experimental security suite and write SARIF: + +```bash +"$CODEQL" database analyze "$DB" \ + "$ROOT/lua/ql/src/codeql-suites/lua-security-experimental.qls" \ + --search-path="$ROOT:$ROOT/lua" \ + --format=sarifv2.1.0 \ + --output=/tmp/lua-results.sarif +``` + +## Analyze source + +Compile each source file into a separate output tree with the same relative +path. Use a new or empty bytecode directory so stale output cannot enter the +database. + +```bash +set -euo pipefail + +SOURCE_ROOT=/absolute/path/to/lua-source +BYTECODE_ROOT=$(mktemp -d) + +find "$SOURCE_ROOT" -type f -name '*.lua' -print0 | +while IFS= read -r -d '' source; do + relative=${source#"$SOURCE_ROOT"/} + output="$BYTECODE_ROOT/${relative%.lua}.luac" + mkdir -p "$(dirname "$output")" + luac5.1 -o "$output" "$source" +done +``` + +Create the database from the compiled tree. The original source tree remains +the compilation input; semantic analysis and database module paths come from +the matching bytecode layout. + +```bash +DB=/tmp/codeql-lua51-source-db + +env -u PYTHONPATH CODEQL_LUA_BYTECODE_INPUT_ROOT="$BYTECODE_ROOT" \ + "$CODEQL" database create "$DB" \ + --overwrite \ + --language=lua \ + --source-root="$BYTECODE_ROOT" \ + --build-mode=none \ + --search-path="$ROOT:$ROOT/lua" +``` + +Use the same `database analyze` command from the bytecode workflow. + +Creating a database directly from a `.lua` source tree without +`CODEQL_LUA_BYTECODE_INPUT_ROOT` records source-file inventory only. It does +not produce source AST, semantic, data-flow, or taint facts. + +## Query interfaces + +The Lua 5.1 analysis capability is mature. Its user-facing queries follow +CodeQL's experimental support lifecycle for initial upstream adoption; this +publication status does not apply to the extractor, schema, or analysis +libraries. The default supported suites intentionally select no Lua query yet. + +| Query | Purpose | Suite status | +| --- | --- | --- | +| [`lua/command-injection`](ql/src/experimental/Security/CWE-078/CommandInjection.ql) | Unsanitized external-data flow to command execution. | Experimental security suite and direct execution. | +| [`lua/diagnostics/sanitized-command-flow`](ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql) | Review paths suppressed by a sanitizer on the relevant route. | Experimental direct/manual diagnostic query. | +| [`lua/diagnostics/bytecode-facts`](ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql) | Detailed tagged bytecode, flow, module, and rule facts. | Experimental direct/manual table query. | + +The active and sanitized queries are path problems with `file`, `source`, +`sink`, and message output plus native path edges. Their formal query help is +next to each query. The facts query returns a tagged union suitable for CSV or +JSON consumers. See [`ql/src/README.md`](ql/src/README.md) for output schemas. + +Run a direct query and decode it as CSV: + +```bash +QUERY="$ROOT/lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql" + +"$CODEQL" query run "$QUERY" \ + --database="$DB" \ + --search-path="$ROOT:$ROOT/lua" \ + --output=/tmp/lua-results.bqrs + +"$CODEQL" bqrs decode /tmp/lua-results.bqrs \ + --format=csv \ + --output=/tmp/lua-results.csv +``` + +## Complex multi-file example + +The committed integration corpus contains 46 source files and 46 matching Lua +5.1 bytecode files in a realistic module layout. Its compact oracle proves +46 accepted bytecode artifacts, zero diagnostics, nine active findings, zero +sanitized findings, and module, interprocedural, and cross-module behavior. + +- [Corpus manifest](ql/test/library-tests/complex-bytecode-integration/SAMPLE-MANIFEST.md) +- [Compact oracle](ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.expected) + +Reproduce this example from the repository root: + +```bash +"$CODEQL" test run \ + lua/ql/test/library-tests/complex-bytecode-integration \ + --search-path="$ROOT:$ROOT/lua" +``` + +## Validation + +Run the complete extractor and QL regression suites: + +```bash +python3 -m unittest discover -s lua/tools/tests -p 'test_*.py' + +"$CODEQL" test run lua/ql/test \ + --search-path="$ROOT:$ROOT/lua" + +"$CODEQL" query compile --check-only --warnings=error \ + --search-path="$ROOT:$ROOT/lua" \ + lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql \ + lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql \ + lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql + +cmp lua/lua.dbscheme lua/ql/lib/lua.dbscheme +cmp lua/lua.dbscheme.stats lua/ql/lib/lua.dbscheme.stats +``` + +The [test matrix](ql/test/library-tests/README.md) lists the stable behavioral +boundaries. Generated databases, reports, BQRS, SARIF, logs, and profiles are +not committed. + +## Input boundary + +The bytecode loader accepts Lua 5.1 chunks with format version 0, either +endianness, positive C `int` and `size_t` widths, 4-byte instructions, and +8-byte floating-point `lua_Number` values. Malformed chunks and unsupported +profiles emit diagnostics instead of partial semantic facts. + +The implementation is self-contained in this repository and does not import +precomputed findings. Production analysis derives results from extracted +bytecode facts and the QL libraries under `lua/ql/lib/codeql/lua`. diff --git a/lua/SCHEMA.md b/lua/SCHEMA.md new file mode 100644 index 000000000000..44b6323b7a12 --- /dev/null +++ b/lua/SCHEMA.md @@ -0,0 +1,74 @@ +# Lua 5.1 bytecode schema + +`lua/lua.dbscheme` is the extractor schema authority. The mirrored schema and +statistics files under `lua/ql/lib` let the QL packs resolve the same relations +and must remain byte-identical to their extractor-package counterparts: + +```bash +cmp lua/lua.dbscheme lua/ql/lib/lua.dbscheme +cmp lua/lua.dbscheme.stats lua/ql/lib/lua.dbscheme.stats +``` + +## Stable identities + +- Artifact and module paths are normalized, source-root-relative paths. +- Prototype IDs use `root` followed by ordinal child components such as + `root.2.1`. +- Instruction sites use `@pc`. +- Register values use `@pc:r`. +- Callsite IDs use the instruction-site form and are always interpreted with + caller module and caller prototype ownership. +- Missing debug names remain explicit as unavailable metadata; they are not + guessed from paths or benchmark results. + +These identities describe input evidence. Production rules derive behavior +from extracted relations and resolved API semantics rather than input +identities. + +## Relation families + +| Family | Relations | Contract | +| --- | --- | --- | +| Files and locations | `files`, `folders`, `containerparent`, `locations_default`, `sourceLocationPrefix` | Minimal entities required for CodeQL result locations. | +| Input inventory | `lua_source_files`, `lua_artifacts`, `lua_profiles`, `lua_diagnostics` | Accepted bytecode and rejected profiles are disjoint. Diagnostic artifacts do not emit partial semantic success facts. | +| Bytecode model | `lua_prototypes`, `lua_instructions`, `lua_constants`, `lua_register_events`, `lua_semantic_steps`, `lua_closure_values`, `lua_call_sites`, `lua_upvalues` | Typed Lua 5.1 structure and register effects. | +| Local semantics | `lua_local_flows`, `lua_control_flow_edges`, `lua_dominator_tree_intervals`, `lua_analysis_boundaries` | Reaching definitions, control flow, dominance, and explicit unsupported boundaries. | +| State carriers | `lua_table_field_flows`, `lua_global_flows`, `lua_upvalue_flows` | Static table fields may be precise; dynamic keys never create precise field flow. Same-table conservative object flow remains available. | +| Resolution | `lua_call_resolutions`, `lua_literal_requires`, `lua_module_resolutions`, `lua_module_exports` | Resolved, ambiguous, and unresolved states remain explicit. No guessed callee or module target is emitted. | +| Interprocedural flow | `lua_interprocedural_flows` | Argument, vararg, return, open-result, and tailcall rows retain caller module, caller prototype, callsite, callee module, and callee prototype ownership. | +| Mapping evidence | `lua_mapping_markers`, `lua_mapping_marker_diagnostics` | Source/bytecode mapping states; not a source AST contract. | + +## Analysis invariants + +1. An input is either accepted or diagnostic; malformed input cannot contribute + ordinary bytecode, flow, or report facts. +2. Local reaching definitions reject overwritten values while preserving + branch and loop alternatives. +3. Static table fields may use field-sensitive flow. Unknown or dynamic keys + use conservative table-object flow and do not acquire a fabricated field + name. +4. Interprocedural edges are callsite-balanced. A return from one callsite + cannot satisfy another callsite's route. +5. Fixed and producer-proven open arguments/results are represented + structurally. Missing producer evidence is an explicit boundary, not a + guessed slot range. +6. Sanitizer and report semantics are derived in QL from typed calls and the + generic flow graph. Extractor relations do not contain final benchmark + findings. + +## Changing the schema + +When a relation changes: + +1. Update both dbscheme copies and both statistics copies identically. +2. Update the TRAP producer in `lua/tools/index_lua_files.py`. +3. Update the typed QL wrapper in `lua/ql/lib/codeql/lua/`. +4. Add or update a committed positive and negative test at the complete + behavioral boundary. +5. Run the Python, QL, query-compile, and schema-mirror commands in + [`README.md`](README.md). + +The schema and QL APIs support the repository-local Lua 5.1 bytecode analysis +implementation. Official bundle and CodeQL Action registration are separate +upstream product integration work. Source parsing and non-Lua-5.1 bytecode are +not claimed. diff --git a/lua/codeql-extractor.yml b/lua/codeql-extractor.yml new file mode 100644 index 000000000000..47570b428283 --- /dev/null +++ b/lua/codeql-extractor.yml @@ -0,0 +1,22 @@ +name: "lua" +display_name: "Lua" +version: 0.1.0 +column_kind: "utf8" +legacy_qltest_extraction: true +build_modes: + - none +default_queries: + - codeql/lua-queries +github_api_languages: + - Lua +scc_languages: + - Lua +file_types: + - name: lua + display_name: Lua source files + extensions: + - .lua + - name: lua-bytecode + display_name: Lua 5.1 bytecode files + extensions: + - .luac diff --git a/lua/lua.dbscheme b/lua/lua.dbscheme new file mode 100644 index 000000000000..e66aad4742fa --- /dev/null +++ b/lua/lua.dbscheme @@ -0,0 +1,315 @@ +files( + unique int id: @file, + string name: string ref +); + +folders( + unique int id: @folder, + string name: string ref +); + +@container = @file | @folder + +containerparent( + int parent: @container ref, + unique int child: @container ref +); + +locations_default( + unique int id: @location_default, + int file: @file ref, + int beginLine: int ref, + int beginColumn: int ref, + int endLine: int ref, + int endColumn: int ref +); + +sourceLocationPrefix( + string prefix: string ref +); + +lua_source_files( + unique int id: @lua_source_file, + int file: @file ref, + string path: string ref, + int line_count: int ref, + int byte_count: int ref, + string sha256: string ref +); + +lua_artifacts( + unique int id: @lua_artifact, + string fixture_id: string ref, + string path: string ref, + string input_kind: string ref, + string profile_id: string ref, + int accepted: int ref, + string provenance: string ref +); + +lua_profiles( + unique int artifact: @lua_artifact ref, + string version: string ref, + int format: int ref, + int little_endian: int ref, + int int_size: int ref, + int size_t_size: int ref, + int instruction_size: int ref, + int lua_number_size: int ref, + int integral_flag: int ref +); + +lua_prototypes( + unique int id: @lua_prototype, + int artifact: @lua_artifact ref, + string fixture_id: string ref, + string prototype_id: string ref, + string parent_prototype_id: string ref, + int ordinal_index: int ref, + int num_params: int ref, + int is_vararg: int ref, + int max_stack: int ref, + int upvalue_count: int ref, + string debug_name: string ref, + string mapping_state: string ref, + string provenance: string ref +); + +lua_instructions( + unique int id: @lua_instruction, + int prototype: @lua_prototype ref, + string fixture_id: string ref, + string prototype_id: string ref, + int pc: int ref, + string opcode: string ref, + int operand_a: int ref, + int operand_b: int ref, + int operand_c: int ref +); + +lua_constants( + unique int id: @lua_constant, + int prototype: @lua_prototype ref, + string fixture_id: string ref, + string constant_id: string ref, + string prototype_id: string ref, + int index: int ref, + string lua_type: string ref, + string value: string ref +); + +lua_register_events( + unique int id: @lua_register_event, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string prototype_id: string ref, + int pc: int ref, + string event_kind: string ref, + int slot_index: int ref, + string value_ref: string ref +); + +lua_semantic_steps( + unique int id: @lua_semantic_step, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string source_ref: string ref, + string dest_ref: string ref, + string step_kind: string ref +); + +lua_closure_values( + unique int id: @lua_closure_value, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string value_ref: string ref, + string target_prototype_id: string ref, + string provenance: string ref +); + +lua_call_sites( + unique int id: @lua_call_site, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string callsite_id: string ref, + string prototype_id: string ref, + int pc: int ref, + string opcode: string ref, + string target_value_ref: string ref, + int first_arg_slot: int ref, + int arg_count: int ref, + int first_return_slot: int ref, + int return_count: int ref +); + +lua_upvalues( + unique int id: @lua_upvalue, + int prototype: @lua_prototype ref, + string fixture_id: string ref, + string upvalue_id: string ref, + string prototype_id: string ref, + int upvalue_index: int ref, + string debug_name: string ref, + string mapping_state: string ref, + string provenance: string ref +); + +lua_local_flows( + unique int id: @lua_local_flow, + string module_path: string ref, + string prototype_id: string ref, + string source_ref: string ref, + string sink_ref: string ref, + string edge_kind: string ref, + string provenance: string ref +); + +lua_control_flow_edges( + unique int id: @lua_control_flow_edge, + int source_instruction: @lua_instruction ref, + int target_instruction: @lua_instruction ref, + string module_path: string ref, + string prototype_id: string ref, + int source_pc: int ref, + int target_pc: int ref, + string provenance: string ref +); + +lua_dominator_tree_intervals( + unique int id: @lua_dominator_tree_interval, + int instruction: @lua_instruction ref, + string module_path: string ref, + string prototype_id: string ref, + int pc: int ref, + int start: int ref, + int end: int ref, + string provenance: string ref +); + +lua_analysis_boundaries( + unique int id: @lua_analysis_boundary, + string module_path: string ref, + string prototype_id: string ref, + string site_id: string ref, + string boundary_kind: string ref, + string reason: string ref, + string provenance: string ref +); + +lua_table_field_flows( + unique int id: @lua_table_field_flow, + string module_path: string ref, + string prototype_id: string ref, + string table_ref: string ref, + string field_name: string ref, + string write_ref: string ref, + string read_ref: string ref, + string provenance: string ref +); + +lua_global_flows( + unique int id: @lua_global_flow, + string fixture_id: string ref, + string global_name: string ref, + string write_ref: string ref, + string read_ref: string ref, + string value_ref: string ref, + string provenance: string ref +); + +lua_upvalue_flows( + unique int id: @lua_upvalue_flow, + string fixture_id: string ref, + string upvalue_id: string ref, + string capture_ref: string ref, + string read_ref: string ref, + string write_ref: string ref, + string provenance: string ref +); + +lua_call_resolutions( + unique int id: @lua_call_resolution, + string caller_module_path: string ref, + string caller_prototype_id: string ref, + string callsite_id: string ref, + string target_value_ref: string ref, + string resolved_name: string ref, + string resolution_kind: string ref, + string target_module_path: string ref, + string target_prototype_id: string ref, + string provenance: string ref +); + +lua_literal_requires( + unique int id: @lua_literal_require, + string caller_module_path: string ref, + string caller_prototype_id: string ref, + string callsite_id: string ref, + string require_string: string ref, + string argument_ref: string ref, + string provenance: string ref +); + +lua_module_resolutions( + unique int id: @lua_module_resolution, + string caller_module_path: string ref, + string callsite_id: string ref, + string require_string: string ref, + string status: string ref, + string target_module_path: string ref, + string reason: string ref, + string provenance: string ref +); + +lua_module_exports( + unique int id: @lua_module_export, + string module_path: string ref, + string export_kind: string ref, + string field_name: string ref, + string value_ref: string ref, + string target_prototype_id: string ref, + string provenance: string ref +); + +lua_interprocedural_flows( + unique int id: @lua_interprocedural_flow, + string caller_module_path: string ref, + string caller_prototype_id: string ref, + string callsite_id: string ref, + string callee_module_path: string ref, + string callee_prototype_id: string ref, + string source_ref: string ref, + string sink_ref: string ref, + string flow_kind: string ref, + int position: int ref, + string provenance: string ref +); + +lua_diagnostics( + unique int id: @lua_diagnostic, + int artifact: @lua_artifact ref, + string fixture_id: string ref, + string diagnostic_id: string ref, + string kind: string ref, + string input_ref: string ref, + string severity: string ref, + string message_category: string ref, + int success_facts_allowed: int ref, + string provenance: string ref +); + +lua_mapping_markers( + unique int id: @lua_mapping_marker, + string fixture_id: string ref, + string mapping_kind: string ref, + string bytecode_ref: string ref, + string state: string ref, + string provenance: string ref +); + +lua_mapping_marker_diagnostics( + unique int id: @lua_mapping_marker_diagnostic, + int marker: @lua_mapping_marker ref, + string fixture_id: string ref, + string diagnostic_kind: string ref +); diff --git a/lua/lua.dbscheme.stats b/lua/lua.dbscheme.stats new file mode 100644 index 000000000000..02d970bb5b5f --- /dev/null +++ b/lua/lua.dbscheme.stats @@ -0,0 +1,109 @@ + + + + @file + 0 + + + @folder + 0 + + + @location_default + 0 + + + @lua_source_file + 0 + + + @lua_artifact + 0 + + + @lua_prototype + 0 + + + @lua_instruction + 0 + + + @lua_constant + 0 + + + @lua_register_event + 0 + + + @lua_semantic_step + 0 + + + @lua_closure_value + 0 + + + @lua_call_site + 0 + + + @lua_upvalue + 0 + + + @lua_local_flow + 0 + + + @lua_analysis_boundary + 0 + + + @lua_table_field_flow + 0 + + + @lua_global_flow + 0 + + + @lua_upvalue_flow + 0 + + + @lua_call_resolution + 0 + + + @lua_literal_require + 0 + + + @lua_module_resolution + 0 + + + @lua_module_export + 0 + + + @lua_interprocedural_flow + 0 + + + @lua_diagnostic + 0 + + + @lua_mapping_marker + 0 + + + @lua_mapping_marker_diagnostic + 0 + + + + diff --git a/lua/ql/lib/change-notes/2026-07-21-lua-bytecode-analysis.md b/lua/ql/lib/change-notes/2026-07-21-lua-bytecode-analysis.md new file mode 100644 index 000000000000..9f870ffc6a5e --- /dev/null +++ b/lua/ql/lib/change-notes/2026-07-21-lua-bytecode-analysis.md @@ -0,0 +1,5 @@ +--- +category: feature +--- +* Added Lua 5.1 bytecode extraction models, local and interprocedural data flow, + module resolution, and source, sink, sanitizer, and reporting APIs. diff --git a/lua/ql/lib/codeql/lua/Bytecode.qll b/lua/ql/lib/codeql/lua/Bytecode.qll new file mode 100644 index 000000000000..ea7699e58c42 --- /dev/null +++ b/lua/ql/lib/codeql/lua/Bytecode.qll @@ -0,0 +1,138 @@ +/** + * Provides the decoded Lua 5.1 bytecode fact model. + * + * The model exposes artifacts, profiles, prototypes, constants, diagnostics, + * and source mappings. Instruction-level and semantic facts are exposed by the + * corresponding analysis libraries. + */ +class LuaArtifact extends @lua_artifact { + LuaArtifact() { lua_artifacts(this, _, _, _, _, _, _) } + + string getFixtureId() { lua_artifacts(this, result, _, _, _, _, _) } + + string getPath() { lua_artifacts(this, _, result, _, _, _, _) } + + string getInputKind() { lua_artifacts(this, _, _, result, _, _, _) } + + string getProfileId() { lua_artifacts(this, _, _, _, result, _, _) } + + predicate isAccepted() { lua_artifacts(this, _, _, _, _, 1, _) } + + string getProvenance() { lua_artifacts(this, _, _, _, _, _, result) } + + string toString() { result = this.getPath() } +} + +class LuaProfile extends @lua_artifact { + LuaProfile() { lua_profiles(this, _, _, _, _, _, _, _, _) } + + LuaArtifact getArtifact() { result = this } + + string getVersion() { lua_profiles(this, result, _, _, _, _, _, _, _) } + + int getFormat() { lua_profiles(this, _, result, _, _, _, _, _, _) } + + int isLittleEndian() { lua_profiles(this, _, _, result, _, _, _, _, _) } + + int getIntSize() { lua_profiles(this, _, _, _, result, _, _, _, _) } + + int getSizeTSize() { lua_profiles(this, _, _, _, _, result, _, _, _) } + + int getInstructionSize() { lua_profiles(this, _, _, _, _, _, result, _, _) } + + int getLuaNumberSize() { lua_profiles(this, _, _, _, _, _, _, result, _) } + + int getIntegralFlag() { lua_profiles(this, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getArtifact().toString() } +} + +class LuaPrototype extends @lua_prototype { + LuaPrototype() { lua_prototypes(this, _, _, _, _, _, _, _, _, _, _, _, _) } + + LuaArtifact getArtifact() { lua_prototypes(this, result, _, _, _, _, _, _, _, _, _, _, _) } + + string getFixtureId() { lua_prototypes(this, _, result, _, _, _, _, _, _, _, _, _, _) } + + string getPrototypeId() { lua_prototypes(this, _, _, result, _, _, _, _, _, _, _, _, _) } + + string getParentPrototypeId() { lua_prototypes(this, _, _, _, result, _, _, _, _, _, _, _, _) } + + int getOrdinalIndex() { lua_prototypes(this, _, _, _, _, result, _, _, _, _, _, _, _) } + + int getNumParams() { lua_prototypes(this, _, _, _, _, _, result, _, _, _, _, _, _) } + + int isVararg() { lua_prototypes(this, _, _, _, _, _, _, result, _, _, _, _, _) } + + int getMaxStack() { lua_prototypes(this, _, _, _, _, _, _, _, result, _, _, _, _) } + + int getUpvalueCount() { lua_prototypes(this, _, _, _, _, _, _, _, _, result, _, _, _) } + + string getDebugName() { lua_prototypes(this, _, _, _, _, _, _, _, _, _, result, _, _) } + + string getMappingState() { lua_prototypes(this, _, _, _, _, _, _, _, _, _, _, result, _) } + + string getProvenance() { lua_prototypes(this, _, _, _, _, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getPrototypeId() } +} + +class LuaConstant extends @lua_constant { + LuaConstant() { lua_constants(this, _, _, _, _, _, _, _) } + + LuaPrototype getPrototype() { lua_constants(this, result, _, _, _, _, _, _) } + + string getFixtureId() { lua_constants(this, _, result, _, _, _, _, _) } + + string getConstantId() { lua_constants(this, _, _, result, _, _, _, _) } + + string getPrototypeId() { lua_constants(this, _, _, _, result, _, _, _) } + + int getIndex() { lua_constants(this, _, _, _, _, result, _, _) } + + string getLuaType() { lua_constants(this, _, _, _, _, _, result, _) } + + string getValue() { lua_constants(this, _, _, _, _, _, _, result) } + + string toString() { result = this.getConstantId() } +} + +class LuaDiagnostic extends @lua_diagnostic { + LuaDiagnostic() { lua_diagnostics(this, _, _, _, _, _, _, _, _, _) } + + LuaArtifact getArtifact() { lua_diagnostics(this, result, _, _, _, _, _, _, _, _) } + + string getFixtureId() { lua_diagnostics(this, _, result, _, _, _, _, _, _, _) } + + string getDiagnosticId() { lua_diagnostics(this, _, _, result, _, _, _, _, _, _) } + + string getKind() { lua_diagnostics(this, _, _, _, result, _, _, _, _, _) } + + string getInputRef() { lua_diagnostics(this, _, _, _, _, result, _, _, _, _) } + + string getSeverity() { lua_diagnostics(this, _, _, _, _, _, result, _, _, _) } + + string getMessageCategory() { lua_diagnostics(this, _, _, _, _, _, _, result, _, _) } + + predicate allowsSuccessFacts() { lua_diagnostics(this, _, _, _, _, _, _, _, 1, _) } + + string getProvenance() { lua_diagnostics(this, _, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getDiagnosticId() } +} + +class LuaMappingMarker extends @lua_mapping_marker { + LuaMappingMarker() { lua_mapping_markers(this, _, _, _, _, _) } + + string getFixtureId() { lua_mapping_markers(this, result, _, _, _, _) } + + string getMappingKind() { lua_mapping_markers(this, _, result, _, _, _) } + + string getBytecodeRef() { lua_mapping_markers(this, _, _, result, _, _) } + + string getState() { lua_mapping_markers(this, _, _, _, result, _) } + + string getProvenance() { lua_mapping_markers(this, _, _, _, _, result) } + + string toString() { result = this.getBytecodeRef() } +} diff --git a/lua/ql/lib/codeql/lua/InterproceduralModuleTaint.qll b/lua/ql/lib/codeql/lua/InterproceduralModuleTaint.qll new file mode 100644 index 000000000000..f6fe8079d2a8 --- /dev/null +++ b/lua/ql/lib/codeql/lua/InterproceduralModuleTaint.qll @@ -0,0 +1,319 @@ +/** + * Provides Lua bytecode interprocedural, module, and taint semantics. + * + * This library derives interprocedural and module relations from + * extractor-owned bytecode and intraprocedural semantic facts. + */ + +import codeql.lua.Bytecode +import codeql.lua.IntraproceduralSemantics + +private predicate acceptedArtifactPath(string fixture, string path) { + exists(LuaArtifact artifact | + artifact.getFixtureId() = fixture and + artifact.getPath() = path and + artifact.isAccepted() + ) +} + +predicate moduleIdentity(string fixture, string modulePath, string moduleName, string provenance) { + acceptedArtifactPath(fixture, modulePath) and + moduleName = modulePath.regexpCapture("(^|.*/)([^/]+)\\.luac$", 2) and + provenance = "bytecode-only,module-path" +} + +predicate literalRequireCall( + string fixture, string modulePath, string prototypeId, int pc, string callsiteId, + string requireString, string argumentValueRef, string provenance +) { + exists(LuaLiteralRequire require, LuaCallSite call | + fixture = require.getCallerModulePath() and + modulePath = require.getCallerModulePath() and + prototypeId = require.getCallerPrototypeId() and + callsiteId = require.getCallsiteId() and + requireString = require.getRequireString() and + argumentValueRef = require.getArgumentRef() and + provenance = require.getProvenance() and + call.getFixtureId() = require.getCallerModulePath() and + call.getCallsiteId() = require.getCallsiteId() and + pc = call.getPc() + ) +} + +predicate moduleResolution( + string fixture, string requireCallsiteId, string requireString, string resolutionStatus, + string fromModulePath, string targetModulePath, string unresolvedReason, string provenance +) { + exists(LuaModuleResolution resolution | + fixture = resolution.getCallerModulePath() and + requireCallsiteId = resolution.getCallsiteId() and + requireString = resolution.getRequireString() and + resolutionStatus = resolution.getStatus() and + fromModulePath = resolution.getCallerModulePath() and + targetModulePath = resolution.getTargetModulePath() and + unresolvedReason = resolution.getReason() and + provenance = resolution.getProvenance() + ) +} + +predicate moduleExport( + string fixture, string modulePath, string exportKind, string fieldName, string valueRef, + string targetPrototypeId, string provenance +) { + exists(LuaModuleExport export | + fixture = export.getModulePath() and + modulePath = export.getModulePath() and + exportKind = export.getExportKind() and + fieldName = export.getFieldName() and + valueRef = export.getValueRef() and + targetPrototypeId = export.getTargetPrototypeId() and + provenance = export.getProvenance() + ) +} + +predicate moduleFieldCallTarget( + string fixture, string fromModulePath, string callsiteId, string fieldName, + string targetModulePath, string targetPrototypeId, string provenance +) { + exists(LuaCallResolution resolution, LuaLiteralRequire require, LuaModuleExport export | + resolution.getResolutionKind() = "module-field-export" and + fixture = resolution.getCallerModulePath() and + fromModulePath = resolution.getCallerModulePath() and + callsiteId = resolution.getCallsiteId() and + targetModulePath = resolution.getTargetModulePath() and + targetPrototypeId = resolution.getTargetPrototypeId() and + provenance = resolution.getProvenance() and + require.getCallerModulePath() = resolution.getCallerModulePath() and + resolution.getResolvedName() = require.getRequireString() + "." + fieldName and + export.getModulePath() = resolution.getTargetModulePath() and + fieldName = export.getFieldName() and + export.getTargetPrototypeId() = resolution.getTargetPrototypeId() + ) +} + +predicate interproceduralArgFlow( + string fixture, string callsiteId, string fromArgumentRef, string targetModulePath, + string targetPrototypeId, string toParameterRef, string provenance +) { + exists(LuaInterproceduralFlow flow | + fixture = flow.getCallerModulePath() and + callsiteId = flow.getCallsiteId() and + fromArgumentRef = flow.getSourceRef() and + targetModulePath = flow.getCalleeModulePath() and + targetPrototypeId = flow.getCalleePrototypeId() and + toParameterRef = flow.getSinkRef() and + provenance = flow.getProvenance() and + ( + flow.getFlowKind() = "argument-to-parameter" or + flow.getFlowKind() = "argument-to-vararg" + ) + ) +} + +predicate interproceduralReturnFlow( + string fixture, string callsiteId, string targetModulePath, string targetPrototypeId, + string calleeReturnRef, string callerResultRef, string provenance +) { + exists(LuaInterproceduralFlow flow | + fixture = flow.getCallerModulePath() and + callsiteId = flow.getCallsiteId() and + targetModulePath = flow.getCalleeModulePath() and + targetPrototypeId = flow.getCalleePrototypeId() and + calleeReturnRef = flow.getSourceRef() and + callerResultRef = flow.getSinkRef() and + provenance = flow.getProvenance() and + flow.getFlowKind() = "return-to-result" + ) +} + +predicate genericFlowStep( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, string edgeKind, + string provenance +) { + exists(LuaLocalFlow flow | + sourceModule = flow.getModulePath() and + sourceRef = flow.getSourceRef() and + sinkModule = flow.getModulePath() and + sinkRef = flow.getSinkRef() and + edgeKind = flow.getEdgeKind() and + provenance = flow.getProvenance() + ) + or + exists(LuaTableFieldFlow flow | + sourceModule = flow.getModulePath() and + sourceRef = flow.getWriteRef() and + sinkModule = flow.getModulePath() and + sinkRef = flow.getReadRef() and + edgeKind = "table-field" and + provenance = flow.getProvenance() + ) + or + exists(LuaInterproceduralFlow flow | + edgeKind = flow.getFlowKind() and + provenance = flow.getProvenance() and + ( + ( + flow.getFlowKind() = "argument-to-parameter" or + flow.getFlowKind() = "argument-to-vararg" + ) and + sourceModule = flow.getCallerModulePath() and + sourceRef = flow.getSourceRef() and + sinkModule = flow.getCalleeModulePath() and + sinkRef = flow.getSinkRef() + or + flow.getFlowKind() = "return-to-result" and + sourceModule = flow.getCalleeModulePath() and + sourceRef = flow.getSourceRef() and + sinkModule = flow.getCallerModulePath() and + sinkRef = flow.getSinkRef() + ) + ) +} + +private predicate nonCallFlowStep(string modulePath, string sourceRef, string sinkRef) { + exists(LuaLocalFlow flow | + flow.getModulePath() = modulePath and + flow.getSourceRef() = sourceRef and + flow.getSinkRef() = sinkRef + ) + or + exists(LuaTableFieldFlow flow | + flow.getModulePath() = modulePath and + flow.getWriteRef() = sourceRef and + flow.getReadRef() = sinkRef + ) +} + +private predicate pairedCallFlows( + LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow +) { + ( + argumentFlow.getFlowKind() = "argument-to-parameter" or + argumentFlow.getFlowKind() = "argument-to-vararg" + ) and + returnFlow.getFlowKind() = "return-to-result" and + argumentFlow.getCallerModulePath() = returnFlow.getCallerModulePath() and + argumentFlow.getCallerPrototypeId() = returnFlow.getCallerPrototypeId() and + argumentFlow.getCallsiteId() = returnFlow.getCallsiteId() and + argumentFlow.getCalleeModulePath() = returnFlow.getCalleeModulePath() and + argumentFlow.getCalleePrototypeId() = returnFlow.getCalleePrototypeId() +} + +private predicate sameLevelFlowStep(string modulePath, string sourceRef, string sinkRef) { + nonCallFlowStep(modulePath, sourceRef, sinkRef) + or + exists(LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow | + pairedCallFlows(argumentFlow, returnFlow) and + argumentFlow.getCallerModulePath() = modulePath and + argumentFlow.getSourceRef() = sourceRef and + returnFlow.getSinkRef() = sinkRef and + ( + argumentFlow.getSinkRef() = returnFlow.getSourceRef() or + sameLevelFlowReachable(argumentFlow.getCalleeModulePath(), argumentFlow.getSinkRef(), + returnFlow.getSourceRef()) + ) + ) +} + +private predicate sameLevelFlowReachable(string modulePath, string sourceRef, string sinkRef) { + sameLevelFlowStep(modulePath, sourceRef, sinkRef) + or + exists(string middleRef | + sameLevelFlowStep(modulePath, sourceRef, middleRef) and + sameLevelFlowReachable(modulePath, middleRef, sinkRef) + ) +} + +predicate sameModuleFlowReachable(string modulePath, string sourceRef, string sinkRef) { + sameLevelFlowReachable(modulePath, sourceRef, sinkRef) +} + +private predicate downwardFlowReachable( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + sourceModule = sinkModule and + sameLevelFlowReachable(sourceModule, sourceRef, sinkRef) + or + exists(LuaInterproceduralFlow argumentFlow | + ( + argumentFlow.getFlowKind() = "argument-to-parameter" or + argumentFlow.getFlowKind() = "argument-to-vararg" + ) and + argumentFlow.getCallerModulePath() = sourceModule and + ( + sourceRef = argumentFlow.getSourceRef() or + sameLevelFlowReachable(sourceModule, sourceRef, argumentFlow.getSourceRef()) + ) and + ( + argumentFlow.getCalleeModulePath() = sinkModule and + argumentFlow.getSinkRef() = sinkRef + or + downwardFlowReachable(argumentFlow.getCalleeModulePath(), argumentFlow.getSinkRef(), + sinkModule, sinkRef) + ) + ) +} + +predicate downwardCallFlowReachable( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + downwardFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) +} + +predicate genericFlowReachable( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + downwardFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) + or + exists(LuaInterproceduralFlow returnFlow | + returnFlow.getFlowKind() = "return-to-result" and + returnFlow.getCalleeModulePath() = sourceModule and + ( + sourceRef = returnFlow.getSourceRef() or + sameLevelFlowReachable(sourceModule, sourceRef, returnFlow.getSourceRef()) + ) and + ( + returnFlow.getCallerModulePath() = sinkModule and + returnFlow.getSinkRef() = sinkRef + or + genericFlowReachable(returnFlow.getCallerModulePath(), returnFlow.getSinkRef(), sinkModule, + sinkRef) + ) + ) +} + +newtype TLuaFlowNode = + MkLuaFlowNode(string modulePath, string valueRef) { + genericFlowStep(modulePath, valueRef, _, _, _, _) + or + genericFlowStep(_, _, modulePath, valueRef, _, _) + } + +class LuaFlowNode extends TLuaFlowNode { + string getModulePath() { this = MkLuaFlowNode(result, _) } + + string getValueRef() { this = MkLuaFlowNode(_, result) } + + string getURL() { + exists(string prefix | + sourceLocationPrefix(prefix) and + result = "file://" + prefix + "/" + this.getModulePath() + ":0:0:0:0" + ) + } + + string toString() { result = this.getModulePath() + "::" + this.getValueRef() } +} + +predicate flowNodeStep(LuaFlowNode source, LuaFlowNode sink, string edgeKind, string provenance) { + genericFlowStep(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef(), edgeKind, provenance) +} + +predicate crossBoundaryCallTargetCandidate( + string fixture, string callsiteId, string targetModulePath, string targetPrototypeId, + string confidence, string provenance +) { + moduleFieldCallTarget(fixture, _, callsiteId, _, targetModulePath, targetPrototypeId, _) and + confidence = "candidate" and + provenance = "bytecode-only,cross-module-module-field-call" +} diff --git a/lua/ql/lib/codeql/lua/IntraproceduralSemantics.qll b/lua/ql/lib/codeql/lua/IntraproceduralSemantics.qll new file mode 100644 index 000000000000..c5c9fd1f7de3 --- /dev/null +++ b/lua/ql/lib/codeql/lua/IntraproceduralSemantics.qll @@ -0,0 +1,486 @@ +/** + * Provides Lua bytecode intraprocedural semantics for Lua 5.1 analysis. + * + * This library exposes native extractor facts for bytecode-level local + * semantics. + */ + +import codeql.lua.Bytecode + +class LuaInstruction extends @lua_instruction { + LuaInstruction() { lua_instructions(this, _, _, _, _, _, _, _, _) } + + LuaPrototype getPrototype() { lua_instructions(this, result, _, _, _, _, _, _, _) } + + string getFixtureId() { lua_instructions(this, _, result, _, _, _, _, _, _) } + + string getPrototypeId() { lua_instructions(this, _, _, result, _, _, _, _, _) } + + int getPc() { lua_instructions(this, _, _, _, result, _, _, _, _) } + + string getOpcode() { lua_instructions(this, _, _, _, _, result, _, _, _) } + + int getOperandA() { lua_instructions(this, _, _, _, _, _, result, _, _) } + + int getOperandB() { lua_instructions(this, _, _, _, _, _, _, result, _) } + + int getOperandC() { lua_instructions(this, _, _, _, _, _, _, _, result) } + + string getInstructionRef() { result = this.getPrototypeId() + "@pc" + this.getPc().toString() } + + string toString() { result = this.getFixtureId() + ":" + this.getInstructionRef() } +} + +class LuaRegisterEvent extends @lua_register_event { + LuaRegisterEvent() { lua_register_events(this, _, _, _, _, _, _, _) } + + LuaInstruction getInstruction() { lua_register_events(this, result, _, _, _, _, _, _) } + + string getFixtureId() { lua_register_events(this, _, result, _, _, _, _, _) } + + string getPrototypeId() { lua_register_events(this, _, _, result, _, _, _, _) } + + int getPc() { lua_register_events(this, _, _, _, result, _, _, _) } + + string getKind() { lua_register_events(this, _, _, _, _, result, _, _) } + + int getSlot() { lua_register_events(this, _, _, _, _, _, result, _) } + + string getValueRef() { lua_register_events(this, _, _, _, _, _, _, result) } + + predicate isRead() { this.getKind() = "read" } + + predicate isWrite() { this.getKind() = "write" } + + string toString() { result = this.getValueRef() } +} + +class LuaSemanticStep extends @lua_semantic_step { + LuaSemanticStep() { lua_semantic_steps(this, _, _, _, _, _) } + + LuaInstruction getInstruction() { lua_semantic_steps(this, result, _, _, _, _) } + + string getFixtureId() { lua_semantic_steps(this, _, result, _, _, _) } + + string getSourceRef() { lua_semantic_steps(this, _, _, result, _, _) } + + string getDestRef() { lua_semantic_steps(this, _, _, _, result, _) } + + string getKind() { lua_semantic_steps(this, _, _, _, _, result) } + + string toString() { result = this.getSourceRef() + " -> " + this.getDestRef() } +} + +class LuaClosureValue extends @lua_closure_value { + LuaClosureValue() { lua_closure_values(this, _, _, _, _, _) } + + LuaInstruction getInstruction() { lua_closure_values(this, result, _, _, _, _) } + + string getFixtureId() { lua_closure_values(this, _, result, _, _, _) } + + string getValueRef() { lua_closure_values(this, _, _, result, _, _) } + + string getTargetPrototypeId() { lua_closure_values(this, _, _, _, result, _) } + + string getProvenance() { lua_closure_values(this, _, _, _, _, result) } + + string toString() { result = this.getValueRef() } +} + +class LuaCallSite extends @lua_call_site { + LuaCallSite() { lua_call_sites(this, _, _, _, _, _, _, _, _, _, _, _) } + + LuaInstruction getInstruction() { lua_call_sites(this, result, _, _, _, _, _, _, _, _, _, _) } + + string getFixtureId() { lua_call_sites(this, _, result, _, _, _, _, _, _, _, _, _) } + + string getCallsiteId() { lua_call_sites(this, _, _, result, _, _, _, _, _, _, _, _) } + + string getPrototypeId() { lua_call_sites(this, _, _, _, result, _, _, _, _, _, _, _) } + + int getPc() { lua_call_sites(this, _, _, _, _, result, _, _, _, _, _, _) } + + string getOpcode() { lua_call_sites(this, _, _, _, _, _, result, _, _, _, _, _) } + + string getTargetValueRef() { lua_call_sites(this, _, _, _, _, _, _, result, _, _, _, _) } + + int getFirstArgSlot() { lua_call_sites(this, _, _, _, _, _, _, _, result, _, _, _) } + + int getArgCount() { lua_call_sites(this, _, _, _, _, _, _, _, _, result, _, _) } + + int getFirstReturnSlot() { lua_call_sites(this, _, _, _, _, _, _, _, _, _, result, _) } + + int getReturnCount() { lua_call_sites(this, _, _, _, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getCallsiteId() } +} + +class LuaUpvalue extends @lua_upvalue { + LuaUpvalue() { lua_upvalues(this, _, _, _, _, _, _, _, _) } + + LuaPrototype getPrototype() { lua_upvalues(this, result, _, _, _, _, _, _, _) } + + string getFixtureId() { lua_upvalues(this, _, result, _, _, _, _, _, _) } + + string getUpvalueId() { lua_upvalues(this, _, _, result, _, _, _, _, _) } + + string getPrototypeId() { lua_upvalues(this, _, _, _, result, _, _, _, _) } + + int getIndex() { lua_upvalues(this, _, _, _, _, result, _, _, _) } + + string getDebugName() { lua_upvalues(this, _, _, _, _, _, result, _, _) } + + string getMappingState() { lua_upvalues(this, _, _, _, _, _, _, result, _) } + + string getProvenance() { lua_upvalues(this, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getUpvalueId() } +} + +class LuaLocalFlow extends @lua_local_flow { + LuaLocalFlow() { lua_local_flows(this, _, _, _, _, _, _) } + + string getModulePath() { lua_local_flows(this, result, _, _, _, _, _) } + + string getPrototypeId() { lua_local_flows(this, _, result, _, _, _, _) } + + string getSourceRef() { lua_local_flows(this, _, _, result, _, _, _) } + + string getSinkRef() { lua_local_flows(this, _, _, _, result, _, _) } + + string getEdgeKind() { lua_local_flows(this, _, _, _, _, result, _) } + + string getProvenance() { lua_local_flows(this, _, _, _, _, _, result) } + + string toString() { result = this.getSourceRef() + " -> " + this.getSinkRef() } +} + +class LuaControlFlowEdge extends @lua_control_flow_edge { + LuaControlFlowEdge() { lua_control_flow_edges(this, _, _, _, _, _, _, _) } + + LuaInstruction getSourceInstruction() { lua_control_flow_edges(this, result, _, _, _, _, _, _) } + + LuaInstruction getTargetInstruction() { lua_control_flow_edges(this, _, result, _, _, _, _, _) } + + string getModulePath() { lua_control_flow_edges(this, _, _, result, _, _, _, _) } + + string getPrototypeId() { lua_control_flow_edges(this, _, _, _, result, _, _, _) } + + int getSourcePc() { lua_control_flow_edges(this, _, _, _, _, result, _, _) } + + int getTargetPc() { lua_control_flow_edges(this, _, _, _, _, _, result, _) } + + string getProvenance() { lua_control_flow_edges(this, _, _, _, _, _, _, result) } + + string toString() { + result = + this.getPrototypeId() + "@pc" + this.getSourcePc().toString() + " -> pc" + + this.getTargetPc().toString() + } +} + +predicate controlFlowStep(LuaInstruction source, LuaInstruction sink) { + exists(LuaControlFlowEdge edge | + source = edge.getSourceInstruction() and sink = edge.getTargetInstruction() + ) +} + +class LuaDominatorTreeInterval extends @lua_dominator_tree_interval { + LuaDominatorTreeInterval() { lua_dominator_tree_intervals(this, _, _, _, _, _, _, _) } + + LuaInstruction getInstruction() { lua_dominator_tree_intervals(this, result, _, _, _, _, _, _) } + + string getModulePath() { lua_dominator_tree_intervals(this, _, result, _, _, _, _, _) } + + string getPrototypeId() { lua_dominator_tree_intervals(this, _, _, result, _, _, _, _) } + + int getPc() { lua_dominator_tree_intervals(this, _, _, _, result, _, _, _) } + + int getStart() { lua_dominator_tree_intervals(this, _, _, _, _, result, _, _) } + + int getEnd() { lua_dominator_tree_intervals(this, _, _, _, _, _, result, _) } + + string getProvenance() { lua_dominator_tree_intervals(this, _, _, _, _, _, _, result) } + + string toString() { + result = + this.getPrototypeId() + "@pc" + this.getPc().toString() + " [" + this.getStart().toString() + + "," + this.getEnd().toString() + "]" + } +} + +predicate instructionDominates(LuaInstruction dominator, LuaInstruction instruction) { + exists(LuaDominatorTreeInterval dominatorInterval, LuaDominatorTreeInterval interval | + dominatorInterval.getInstruction() = dominator and + interval.getInstruction() = instruction and + dominatorInterval.getModulePath() = interval.getModulePath() and + dominatorInterval.getPrototypeId() = interval.getPrototypeId() and + dominatorInterval.getStart() <= interval.getStart() and + interval.getStart() <= dominatorInterval.getEnd() + ) +} + +class LuaAnalysisBoundary extends @lua_analysis_boundary { + LuaAnalysisBoundary() { lua_analysis_boundaries(this, _, _, _, _, _, _) } + + string getModulePath() { lua_analysis_boundaries(this, result, _, _, _, _, _) } + + string getPrototypeId() { lua_analysis_boundaries(this, _, result, _, _, _, _) } + + string getSiteId() { lua_analysis_boundaries(this, _, _, result, _, _, _) } + + string getBoundaryKind() { lua_analysis_boundaries(this, _, _, _, result, _, _) } + + string getReason() { lua_analysis_boundaries(this, _, _, _, _, result, _) } + + string getProvenance() { lua_analysis_boundaries(this, _, _, _, _, _, result) } + + string toString() { result = this.getSiteId() + ":" + this.getBoundaryKind() } +} + +class LuaTableFieldFlow extends @lua_table_field_flow { + LuaTableFieldFlow() { lua_table_field_flows(this, _, _, _, _, _, _, _) } + + string getModulePath() { lua_table_field_flows(this, result, _, _, _, _, _, _) } + + string getPrototypeId() { lua_table_field_flows(this, _, result, _, _, _, _, _) } + + string getTableRef() { lua_table_field_flows(this, _, _, result, _, _, _, _) } + + string getFieldName() { lua_table_field_flows(this, _, _, _, result, _, _, _) } + + string getWriteRef() { lua_table_field_flows(this, _, _, _, _, result, _, _) } + + string getReadRef() { lua_table_field_flows(this, _, _, _, _, _, result, _) } + + string getProvenance() { lua_table_field_flows(this, _, _, _, _, _, _, result) } + + string toString() { result = this.getWriteRef() + " -> " + this.getReadRef() } +} + +class LuaGlobalFlow extends @lua_global_flow { + LuaGlobalFlow() { lua_global_flows(this, _, _, _, _, _, _) } + + string getFixtureId() { lua_global_flows(this, result, _, _, _, _, _) } + + string getGlobalName() { lua_global_flows(this, _, result, _, _, _, _) } + + string getWriteRef() { lua_global_flows(this, _, _, result, _, _, _) } + + string getReadRef() { lua_global_flows(this, _, _, _, result, _, _) } + + string getValueRef() { lua_global_flows(this, _, _, _, _, result, _) } + + string getProvenance() { lua_global_flows(this, _, _, _, _, _, result) } + + string toString() { result = this.getGlobalName() } +} + +class LuaUpvalueFlow extends @lua_upvalue_flow { + LuaUpvalueFlow() { lua_upvalue_flows(this, _, _, _, _, _, _) } + + string getFixtureId() { lua_upvalue_flows(this, result, _, _, _, _, _) } + + string getUpvalueId() { lua_upvalue_flows(this, _, result, _, _, _, _) } + + string getCaptureRef() { lua_upvalue_flows(this, _, _, result, _, _, _) } + + string getReadRef() { lua_upvalue_flows(this, _, _, _, result, _, _) } + + string getWriteRef() { lua_upvalue_flows(this, _, _, _, _, result, _) } + + string getProvenance() { lua_upvalue_flows(this, _, _, _, _, _, result) } + + string toString() { result = this.getUpvalueId() } +} + +class LuaCallResolution extends @lua_call_resolution { + LuaCallResolution() { lua_call_resolutions(this, _, _, _, _, _, _, _, _, _) } + + string getCallerModulePath() { lua_call_resolutions(this, result, _, _, _, _, _, _, _, _) } + + string getCallerPrototypeId() { lua_call_resolutions(this, _, result, _, _, _, _, _, _, _) } + + string getCallsiteId() { lua_call_resolutions(this, _, _, result, _, _, _, _, _, _) } + + string getTargetValueRef() { lua_call_resolutions(this, _, _, _, result, _, _, _, _, _) } + + string getResolvedName() { lua_call_resolutions(this, _, _, _, _, result, _, _, _, _) } + + string getResolutionKind() { lua_call_resolutions(this, _, _, _, _, _, result, _, _, _) } + + string getTargetModulePath() { lua_call_resolutions(this, _, _, _, _, _, _, result, _, _) } + + string getTargetPrototypeId() { lua_call_resolutions(this, _, _, _, _, _, _, _, result, _) } + + string getProvenance() { lua_call_resolutions(this, _, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getCallsiteId() + ":" + this.getResolutionKind() } +} + +class LuaLiteralRequire extends @lua_literal_require { + LuaLiteralRequire() { lua_literal_requires(this, _, _, _, _, _, _) } + + string getCallerModulePath() { lua_literal_requires(this, result, _, _, _, _, _) } + + string getCallerPrototypeId() { lua_literal_requires(this, _, result, _, _, _, _) } + + string getCallsiteId() { lua_literal_requires(this, _, _, result, _, _, _) } + + string getRequireString() { lua_literal_requires(this, _, _, _, result, _, _) } + + string getArgumentRef() { lua_literal_requires(this, _, _, _, _, result, _) } + + string getProvenance() { lua_literal_requires(this, _, _, _, _, _, result) } + + string toString() { result = this.getCallsiteId() + ":" + this.getRequireString() } +} + +class LuaModuleResolution extends @lua_module_resolution { + LuaModuleResolution() { lua_module_resolutions(this, _, _, _, _, _, _, _) } + + string getCallerModulePath() { lua_module_resolutions(this, result, _, _, _, _, _, _) } + + string getCallsiteId() { lua_module_resolutions(this, _, result, _, _, _, _, _) } + + string getRequireString() { lua_module_resolutions(this, _, _, result, _, _, _, _) } + + string getStatus() { lua_module_resolutions(this, _, _, _, result, _, _, _) } + + string getTargetModulePath() { lua_module_resolutions(this, _, _, _, _, result, _, _) } + + string getReason() { lua_module_resolutions(this, _, _, _, _, _, result, _) } + + string getProvenance() { lua_module_resolutions(this, _, _, _, _, _, _, result) } + + string toString() { result = this.getCallsiteId() + ":" + this.getStatus() } +} + +class LuaModuleExport extends @lua_module_export { + LuaModuleExport() { lua_module_exports(this, _, _, _, _, _, _) } + + string getModulePath() { lua_module_exports(this, result, _, _, _, _, _) } + + string getExportKind() { lua_module_exports(this, _, result, _, _, _, _) } + + string getFieldName() { lua_module_exports(this, _, _, result, _, _, _) } + + string getValueRef() { lua_module_exports(this, _, _, _, result, _, _) } + + string getTargetPrototypeId() { lua_module_exports(this, _, _, _, _, result, _) } + + string getProvenance() { lua_module_exports(this, _, _, _, _, _, result) } + + string toString() { result = this.getExportKind() + ":" + this.getFieldName() } +} + +class LuaInterproceduralFlow extends @lua_interprocedural_flow { + LuaInterproceduralFlow() { lua_interprocedural_flows(this, _, _, _, _, _, _, _, _, _, _) } + + string getCallerModulePath() { + lua_interprocedural_flows(this, result, _, _, _, _, _, _, _, _, _) + } + + string getCallerPrototypeId() { + lua_interprocedural_flows(this, _, result, _, _, _, _, _, _, _, _) + } + + string getCallsiteId() { lua_interprocedural_flows(this, _, _, result, _, _, _, _, _, _, _) } + + string getCalleeModulePath() { + lua_interprocedural_flows(this, _, _, _, result, _, _, _, _, _, _) + } + + string getCalleePrototypeId() { + lua_interprocedural_flows(this, _, _, _, _, result, _, _, _, _, _) + } + + string getSourceRef() { lua_interprocedural_flows(this, _, _, _, _, _, result, _, _, _, _) } + + string getSinkRef() { lua_interprocedural_flows(this, _, _, _, _, _, _, result, _, _, _) } + + string getFlowKind() { lua_interprocedural_flows(this, _, _, _, _, _, _, _, result, _, _) } + + int getPosition() { lua_interprocedural_flows(this, _, _, _, _, _, _, _, _, result, _) } + + string getProvenance() { lua_interprocedural_flows(this, _, _, _, _, _, _, _, _, _, result) } + + string toString() { result = this.getSourceRef() + " -> " + this.getSinkRef() } +} + +predicate localFlowStep(string fixture, string source, string sink, string kind) { + exists(LuaLocalFlow flow | + flow.getModulePath() = fixture and + source = flow.getSourceRef() and + sink = flow.getSinkRef() and + kind = flow.getEdgeKind() + ) +} + +predicate localFlowReachable(string fixture, string source, string sink) { + localFlowStep(fixture, source, sink, _) + or + exists(string mid | + localFlowStep(fixture, source, mid, _) and + localFlowReachable(fixture, mid, sink) + ) +} + +predicate tableFieldFlow( + string fixture, string tableRef, string fieldName, string writeRef, string readRef +) { + exists(LuaTableFieldFlow flow | + flow.getModulePath() = fixture and + tableRef = flow.getTableRef() and + fieldName = flow.getFieldName() and + writeRef = flow.getWriteRef() and + readRef = flow.getReadRef() + ) +} + +predicate globalFlowStep( + string fixture, string globalName, string writeRef, string readRef, string valueRef +) { + exists(LuaGlobalFlow flow | + flow.getFixtureId() = fixture and + globalName = flow.getGlobalName() and + writeRef = flow.getWriteRef() and + readRef = flow.getReadRef() and + valueRef = flow.getValueRef() + ) +} + +predicate upvalueFlowStep( + string fixture, string upvalueId, string captureRef, string readRef, string writeRef +) { + exists(LuaUpvalueFlow flow | + flow.getFixtureId() = fixture and + upvalueId = flow.getUpvalueId() and + captureRef = flow.getCaptureRef() and + readRef = flow.getReadRef() and + writeRef = flow.getWriteRef() + ) +} + +predicate callResolution( + string callerModulePath, string callsiteId, string targetModulePath, string targetPrototypeId, + string resolvedName, string resolutionKind +) { + exists(LuaCallResolution resolution | + resolution.getCallerModulePath() = callerModulePath and + resolution.getCallsiteId() = callsiteId and + resolution.getTargetModulePath() = targetModulePath and + resolution.getTargetPrototypeId() = targetPrototypeId and + resolution.getResolvedName() = resolvedName and + resolution.getResolutionKind() = resolutionKind + ) +} + +predicate unresolvedCallBoundary(string modulePath, string callsiteId, string reason) { + exists(LuaAnalysisBoundary boundary | + boundary.getModulePath() = modulePath and + boundary.getSiteId() = callsiteId and + boundary.getBoundaryKind() = "unresolved-call-target" and + boundary.getReason() = reason + ) +} diff --git a/lua/ql/lib/codeql/lua/RulesSanitizerReport.qll b/lua/ql/lib/codeql/lua/RulesSanitizerReport.qll new file mode 100644 index 000000000000..5be94d0f49ff --- /dev/null +++ b/lua/ql/lib/codeql/lua/RulesSanitizerReport.qll @@ -0,0 +1,769 @@ +/** + * Provides Lua bytecode rules, sanitizer, and report semantics. + * + * This library builds report semantics from typed bytecode call sites and the + * native generic flow graph. + */ + +import codeql.lua.IntraproceduralSemantics +import codeql.lua.InterproceduralModuleTaint + +private predicate sourceRuleName(string matchedName, string trigger) { + matchedName = "formvalue" and + trigger = "*.formvalue" + or + matchedName = "source" and + trigger = "*.source" +} + +private predicate sinkRuleName(string matchedName, string trigger) { + matchedName = "execute" and trigger = "*.execute" + or + matchedName = "fork_exec" and trigger = "*.fork_exec" + or + matchedName = "fork_call" and trigger = "*.fork_call" + or + matchedName = "call" and trigger = "*.call" + or + matchedName = "exec" and trigger = "*.exec" + or + matchedName = "popen" and trigger = "*.popen" + or + matchedName = "execute_cmd" and trigger = "*.execute_cmd" + or + matchedName = "forkExec" and trigger = "*.forkExec" + or + matchedName = "execi" and trigger = "*.execi" +} + +private predicate sanitizerRuleName(string matchedName) { + matchedName = "shellquote" or + matchedName = "tonumber" or + matchedName = "parseCmdline" or + matchedName = "_strformat" or + matchedName = "_cmdformat" or + matchedName = "macaddr" or + matchedName = "macFormat" or + matchedName = "ip4addr" or + matchedName = "injection_test" or + matchedName = "check_iface_name" or + matchedName = "includeQuote" or + matchedName = "checkTime" or + matchedName = "doShell" or + matchedName = "checkIp" or + matchedName = "includeXxs" or + matchedName = "filterExecShell" or + matchedName = "binaryBase64Enc" or + matchedName = "decCiphertext" or + matchedName = "sha256" or + matchedName = "setMacFilter" or + matchedName = "setIpFilter" or + matchedName = "apcli_get_connect" or + matchedName = "setWifiAPMode" or + matchedName = "cmdSafeCheck" or + matchedName = "checkLanIpMask" or + matchedName = "ipaddr" or + matchedName = "lan_wan_ip_conflict_chk" or + matchedName = "licenseActivated" or + matchedName = "is_activated" or + matchedName = "check_mac" or + matchedName = "set_mac_filter" or + matchedName = "encode" or + matchedName = "getDstRule" or + matchedName = "local_dev_data_check" or + matchedName = "stat" or + matchedName = "check_if_whitelist_opcode" or + matchedName = "param_safety_check" or + matchedName = "sqlite3_db_execute" or + matchedName = "getStorageMountPathByUuid" or + matchedName = "getWanIfname" or + matchedName = "hackCharsCheck" or + matchedName = "open" or + matchedName = "del" or + matchedName = "ip4mac" or + matchedName = "match" or + matchedName = "r29_0" or + matchedName = "r3_0" or + matchedName = "filePathGet" or + matchedName = "checkPort" or + matchedName = "setPTRules" or + matchedName = "apcli_get_ifname_form_band" +} + +private predicate typedRuleCall( + string fixture, string prototypeId, string callsiteId, string matchedName +) { + exists(LuaCallResolution resolution | + resolution.getCallerModulePath() = fixture and + resolution.getCallerPrototypeId() = prototypeId and + resolution.getCallsiteId() = callsiteId and + resolution.getResolvedName() != "" and + matchedName = resolution.getResolvedName().regexpCapture("(^|.*\\.)([^.]+)$", 2) + ) +} + +private predicate sourceCall( + string fixture, string prototypeId, string callsiteId, string matchedName, int parameterIndex, + string sourceRef +) { + exists(LuaCallSite call, LuaRegisterEvent resultWrite | + call.getFixtureId() = fixture and + call.getPrototypeId() = prototypeId and + call.getCallsiteId() = callsiteId and + resultWrite.getInstruction() = call.getInstruction() and + resultWrite.isWrite() and + resultWrite.getSlot() = call.getFirstReturnSlot() and + sourceRef = resultWrite.getValueRef() and + sourceRuleName(matchedName, _) and + typedRuleCall(fixture, prototypeId, callsiteId, matchedName) and + parameterIndex = -1 + ) +} + +private predicate sinkCall( + string fixture, string prototypeId, string callsiteId, string matchedName, int parameterIndex, + string sinkRef +) { + exists(LuaCallSite call, LuaRegisterEvent argumentRead | + call.getFixtureId() = fixture and + call.getPrototypeId() = prototypeId and + call.getCallsiteId() = callsiteId and + argumentRead.getInstruction() = call.getInstruction() and + argumentRead.isRead() and + argumentRead.getSlot() = call.getFirstArgSlot() and + sinkRef = argumentRead.getValueRef() and + sinkRuleName(matchedName, _) and + typedRuleCall(fixture, prototypeId, callsiteId, matchedName) and + parameterIndex = 0 and + not uniqueConcreteFixedStringArgument(fixture, prototypeId, sinkRef) + ) +} + +private predicate uniqueConcreteFixedStringArgument( + string fixture, string prototypeId, string argumentRef +) { + exists(LuaLocalFlow flow, LuaRegisterEvent loadWrite, LuaInstruction load, LuaConstant constant | + flow.getModulePath() = fixture and + flow.getPrototypeId() = prototypeId and + flow.getSinkRef() = argumentRef and + flow.getEdgeKind() = "reaching-definition" and + loadWrite.getValueRef() = flow.getSourceRef() and + loadWrite.isWrite() and + loadWrite.getInstruction() = load and + load.getFixtureId() = fixture and + load.getPrototypeId() = prototypeId and + load.getOpcode() = "LOADK" and + loadWrite.getSlot() = load.getOperandA() and + constant.getFixtureId() = fixture and + constant.getPrototypeId() = prototypeId and + constant.getLuaType() = "string" and + constant.getIndex() = load.getOperandB() and + not exists(LuaLocalFlow other | + other.getModulePath() = fixture and + other.getPrototypeId() = prototypeId and + other.getSinkRef() = argumentRef and + other.getEdgeKind() = "reaching-definition" and + other.getSourceRef() != flow.getSourceRef() + ) + ) +} + +predicate sourceSinkRuleMatch( + string fixture, string callsiteId, string ruleKind, string trigger, string matchedName, + int parameterIndex, string provenance +) { + sourceCall(fixture, _, callsiteId, matchedName, parameterIndex, _) and + sourceRuleName(matchedName, trigger) and + ruleKind = "source" and + provenance = "bytecode-only,ql-source-sink-rule,typed-call-resolution" + or + sinkCall(fixture, _, callsiteId, matchedName, parameterIndex, _) and + sinkRuleName(matchedName, trigger) and + ruleKind = "sink" and + provenance = "bytecode-only,ql-source-sink-rule,typed-call-resolution" +} + +predicate sourceEndpoint( + string fixture, string sourceRef, string callsiteId, string trigger, string provenance +) { + exists(string matchedName | + sourceCall(fixture, _, callsiteId, matchedName, _, sourceRef) and + sourceRuleName(matchedName, trigger) and + provenance = "bytecode-only,ql-source-rule,typed-call-resolution" + ) +} + +predicate sinkEndpoint( + string fixture, string sinkRef, string callsiteId, string trigger, int parameterIndex, + string provenance +) { + exists(string matchedName | + sinkCall(fixture, _, callsiteId, matchedName, parameterIndex, sinkRef) and + sinkRuleName(matchedName, trigger) and + provenance = "bytecode-only,ql-sink-rule,typed-call-resolution" + ) +} + +predicate sanitizerCall( + string fixture, string callsiteId, string sanitizerName, string sanitizedValueRef, + string provenance +) { + exists(LuaCallSite call, LuaCallResolution resolution | + fixture = call.getFixtureId() and + callsiteId = call.getCallsiteId() and + resolution.getCallerModulePath() = call.getFixtureId() and + resolution.getCallerPrototypeId() = call.getPrototypeId() and + resolution.getCallsiteId() = call.getCallsiteId() and + resolution.getResolvedName() != "" and + sanitizerName = resolution.getResolvedName().regexpCapture("(^|.*\\.)([^.]+)$", 2) and + sanitizerRuleName(sanitizerName) and + ( + exists(LuaRegisterEvent resultWrite | + resultWrite.getInstruction() = call.getInstruction() and + resultWrite.isWrite() and + resultWrite.getSlot() = call.getFirstReturnSlot() and + sanitizedValueRef = resultWrite.getValueRef() + ) + or + not exists(LuaRegisterEvent resultWrite | + resultWrite.getInstruction() = call.getInstruction() and + resultWrite.isWrite() and + resultWrite.getSlot() = call.getFirstReturnSlot() + ) and + sanitizedValueRef = "" + ) and + provenance = "bytecode-only,ql-sanitizer-call,typed-call-resolution" + ) +} + +private predicate sanitizerOutput(string modulePath, string valueRef) { + sanitizerCall(modulePath, _, _, valueRef, _) and + valueRef != "" +} + +private predicate sanitizerDerivedFieldWrite(string modulePath, string writeRef) { + exists(string sanitizerValueRef | + sanitizerOutput(modulePath, sanitizerValueRef) and + genericFlowReachable(modulePath, sanitizerValueRef, modulePath, writeRef) + ) +} + +private predicate fullySanitizedStaticFieldRead(string modulePath, string readRef) { + exists(LuaTableFieldFlow flow | + flow.getModulePath() = modulePath and + flow.getReadRef() = readRef and + sanitizerDerivedFieldWrite(modulePath, flow.getWriteRef()) and + not exists(LuaTableFieldFlow other | + other.getModulePath() = flow.getModulePath() and + other.getTableRef() = flow.getTableRef() and + other.getFieldName() = flow.getFieldName() and + other.getReadRef() = flow.getReadRef() and + not sanitizerDerivedFieldWrite(modulePath, other.getWriteRef()) + ) + ) +} + +predicate activeReportFlowStep( + LuaFlowNode source, LuaFlowNode sink, string edgeKind, string provenance +) { + flowNodeStep(source, sink, edgeKind, provenance) and + unsanitizedFlowStep(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef()) +} + +predicate activeReportPath( + LuaFlowNode source, LuaFlowNode sink, string classification, string reason, string provenance +) { + sourceEndpoint(source.getModulePath(), source.getValueRef(), _, _, _) and + sinkEndpoint(sink.getModulePath(), sink.getValueRef(), _, _, _, _) and + unsanitizedFlowReachable(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef()) and + classification = "true-positive" and + reason = "unsanitized active source-to-sink path" and + provenance = "bytecode-only,ql-native-active-report-path" +} + +private predicate unsanitizedFlowStep( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + genericFlowStep(sourceModule, sourceRef, sinkModule, sinkRef, _, _) and + not sanitizerOutput(sinkModule, sinkRef) +} + +private predicate unsanitizedNonCallFlowStep(string modulePath, string sourceRef, string sinkRef) { + exists(string edgeKind | + genericFlowStep(modulePath, sourceRef, modulePath, sinkRef, edgeKind, _) and + edgeKind != "argument-to-parameter" and + edgeKind != "argument-to-vararg" and + edgeKind != "return-to-result" and + not ( + edgeKind = "same-instruction-dependence" and + resolvedCallLocalSummary(modulePath, sourceRef, sinkRef) + ) and + not ( + edgeKind = "same-instruction-dependence" and + fullySanitizedStaticFieldRead(modulePath, sinkRef) + ) and + not sanitizerOutput(modulePath, sinkRef) + ) +} + +private predicate resolvedCallLocalSummary(string modulePath, string sourceRef, string sinkRef) { + exists(LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow | + pairedCallFlows(argumentFlow, returnFlow) and + argumentFlow.getCallerModulePath() = modulePath and + argumentFlow.getSourceRef() = sourceRef and + returnFlow.getSinkRef() = sinkRef + ) +} + +private predicate pairedCallFlows( + LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow +) { + ( + argumentFlow.getFlowKind() = "argument-to-parameter" or + argumentFlow.getFlowKind() = "argument-to-vararg" + ) and + returnFlow.getFlowKind() = "return-to-result" and + argumentFlow.getCallerModulePath() = returnFlow.getCallerModulePath() and + argumentFlow.getCallerPrototypeId() = returnFlow.getCallerPrototypeId() and + argumentFlow.getCallsiteId() = returnFlow.getCallsiteId() and + argumentFlow.getCalleeModulePath() = returnFlow.getCalleeModulePath() and + argumentFlow.getCalleePrototypeId() = returnFlow.getCalleePrototypeId() +} + +private predicate unsanitizedPairedCallFlows( + LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow +) { + pairedCallFlows(argumentFlow, returnFlow) and + not guardSanitizerBeforeRefUse(argumentFlow.getCalleeModulePath(), argumentFlow.getSinkRef(), + returnFlow.getSourceRef()) +} + +private predicate guardSanitizerBeforeUse(string modulePath, string sourceRef, LuaInstruction use) { + exists( + LuaCallSite sanitizer, LuaRegisterEvent argumentRead, string sanitizerName, + string sanitizedValueRef + | + sanitizer.getFixtureId() = modulePath and + sanitizerCall(modulePath, sanitizer.getCallsiteId(), sanitizerName, sanitizedValueRef, _) and + sanitizerArgumentRead(sanitizer, argumentRead) and + sameModuleFlowReachable(modulePath, sourceRef, argumentRead.getValueRef()) and + instructionDominates(sanitizer.getInstruction(), use) + ) +} + +private predicate sanitizerArgumentRead(LuaCallSite sanitizer, LuaRegisterEvent argumentRead) { + argumentRead.getInstruction() = sanitizer.getInstruction() and + argumentRead.isRead() and + argumentRead.getSlot() >= sanitizer.getFirstArgSlot() and + argumentRead.getSlot() < sanitizer.getFirstArgSlot() + sanitizer.getArgCount() +} + +private predicate guardSanitizerBeforeRefUse(string modulePath, string sourceRef, string useRef) { + exists(LuaRegisterEvent useRead | + useRead.getValueRef() = useRef and + useRead.isRead() and + useRead.getInstruction().getFixtureId() = modulePath and + guardSanitizerBeforeUse(modulePath, sourceRef, useRead.getInstruction()) + ) +} + +private predicate unsanitizedSameLevelFlowStep(string modulePath, string sourceRef, string sinkRef) { + unsanitizedNonCallFlowStep(modulePath, sourceRef, sinkRef) + or + exists(LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow | + unsanitizedPairedCallFlows(argumentFlow, returnFlow) and + argumentFlow.getCallerModulePath() = modulePath and + argumentFlow.getSourceRef() = sourceRef and + returnFlow.getSinkRef() = sinkRef and + not sanitizerOutput(argumentFlow.getCalleeModulePath(), argumentFlow.getSinkRef()) and + not sanitizerOutput(modulePath, returnFlow.getSinkRef()) and + ( + argumentFlow.getSinkRef() = returnFlow.getSourceRef() or + unsanitizedSameLevelFlowReachable(argumentFlow.getCalleeModulePath(), + argumentFlow.getSinkRef(), returnFlow.getSourceRef()) + ) + ) +} + +private predicate unsanitizedSameLevelFlowReachable( + string modulePath, string sourceRef, string sinkRef +) { + unsanitizedSameLevelFlowStep(modulePath, sourceRef, sinkRef) + or + exists(string middleRef | + unsanitizedSameLevelFlowStep(modulePath, sourceRef, middleRef) and + unsanitizedSameLevelFlowReachable(modulePath, middleRef, sinkRef) + ) +} + +private predicate unsanitizedDownwardFlowReachable( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + sourceModule = sinkModule and + unsanitizedSameLevelFlowReachable(sourceModule, sourceRef, sinkRef) and + not guardSanitizerBeforeRefUse(sourceModule, sourceRef, sinkRef) + or + exists(LuaInterproceduralFlow argumentFlow | + ( + argumentFlow.getFlowKind() = "argument-to-parameter" or + argumentFlow.getFlowKind() = "argument-to-vararg" + ) and + argumentFlow.getCallerModulePath() = sourceModule and + not sanitizerOutput(argumentFlow.getCalleeModulePath(), argumentFlow.getSinkRef()) and + ( + sourceRef = argumentFlow.getSourceRef() or + unsanitizedSameLevelFlowReachable(sourceModule, sourceRef, argumentFlow.getSourceRef()) + ) and + not guardSanitizerBeforeRefUse(sourceModule, sourceRef, argumentFlow.getSourceRef()) and + ( + argumentFlow.getCalleeModulePath() = sinkModule and + argumentFlow.getSinkRef() = sinkRef + or + unsanitizedDownwardFlowReachable(argumentFlow.getCalleeModulePath(), + argumentFlow.getSinkRef(), sinkModule, sinkRef) + ) + ) +} + +private predicate unsanitizedFlowReachable( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + unsanitizedDownwardFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) + or + exists(LuaInterproceduralFlow returnFlow | + returnFlow.getFlowKind() = "return-to-result" and + returnFlow.getCalleeModulePath() = sourceModule and + not sanitizerOutput(sourceModule, returnFlow.getSourceRef()) and + not sanitizerOutput(returnFlow.getCallerModulePath(), returnFlow.getSinkRef()) and + ( + sourceRef = returnFlow.getSourceRef() or + unsanitizedSameLevelFlowReachable(sourceModule, sourceRef, returnFlow.getSourceRef()) + ) and + not guardSanitizerBeforeRefUse(sourceModule, sourceRef, returnFlow.getSourceRef()) and + ( + returnFlow.getCallerModulePath() = sinkModule and + returnFlow.getSinkRef() = sinkRef + or + unsanitizedFlowReachable(returnFlow.getCallerModulePath(), returnFlow.getSinkRef(), + sinkModule, sinkRef) + ) + ) +} + +private predicate sanitizerResultOnChain( + string sourceModule, string sourceRef, string sanitizerModule, string sanitizedValueRef, + string sinkModule, string sinkRef +) { + sanitizedValueRef != "" and + genericFlowReachable(sourceModule, sourceRef, sanitizerModule, sanitizedValueRef) and + genericFlowReachable(sanitizerModule, sanitizedValueRef, sinkModule, sinkRef) +} + +bindingset[sourceModule, sourceRef, sanitizerModule, sanitizerCallsiteId, sanitizedValueRef, + sinkModule, sinkRef] +private predicate sanitizerResultOnIndependentCallRoute( + string sourceModule, string sourceRef, string sanitizerModule, string sanitizerCallsiteId, + string sanitizedValueRef, string sinkModule, string sinkRef +) { + exists(LuaCallSite sanitizer, LuaInterproceduralFlow routeEdge | + sanitizer.getFixtureId() = sanitizerModule and + sanitizer.getCallsiteId() = sanitizerCallsiteId and + sanitizerDownwardFlowReachable(sourceModule, sourceRef, sanitizerModule, sanitizedValueRef) and + ( + ( + routeEdge.getFlowKind() = "argument-to-parameter" or + routeEdge.getFlowKind() = "argument-to-vararg" + ) and + routeEdge.getCallerModulePath() = sanitizerModule and + routeEdge.getCallerPrototypeId() = sanitizer.getPrototypeId() and + ( + sanitizedValueRef = routeEdge.getSourceRef() or + sanitizerResultSameLevelFlowReachable(sanitizerModule, sanitizedValueRef, + routeEdge.getSourceRef()) + ) and + ( + routeEdge.getCalleeModulePath() = sinkModule and + routeEdge.getSinkRef() = sinkRef + or + sanitizerDownwardFlowReachable(routeEdge.getCalleeModulePath(), routeEdge.getSinkRef(), + sinkModule, sinkRef) + ) and + not unsanitizedRouteUsesCallEdge(sourceModule, sourceRef, sinkModule, sinkRef, routeEdge) + or + routeEdge.getFlowKind() = "return-to-result" and + routeEdge.getCalleeModulePath() = sanitizerModule and + routeEdge.getCalleePrototypeId() = sanitizer.getPrototypeId() and + ( + sanitizedValueRef = routeEdge.getSourceRef() or + sanitizerResultSameLevelFlowReachable(sanitizerModule, sanitizedValueRef, + routeEdge.getSourceRef()) + ) and + ( + routeEdge.getCallerModulePath() = sinkModule and + routeEdge.getSinkRef() = sinkRef + or + sanitizerDownwardFlowReachable(routeEdge.getCallerModulePath(), routeEdge.getSinkRef(), + sinkModule, sinkRef) + ) and + not unsanitizedRouteUsesReturnEdge(sourceModule, sourceRef, sinkModule, sinkRef, routeEdge) + ) + ) +} + +private predicate sanitizerRouteNonCallFlowStep(string modulePath, string sourceRef, string sinkRef) { + exists(string edgeKind | + genericFlowStep(modulePath, sourceRef, modulePath, sinkRef, edgeKind, _) and + edgeKind != "argument-to-parameter" and + edgeKind != "argument-to-vararg" and + edgeKind != "return-to-result" and + not ( + edgeKind = "same-instruction-dependence" and + resolvedCallLocalSummary(modulePath, sourceRef, sinkRef) and + not sanitizerOutput(modulePath, sinkRef) + ) + ) +} + +private predicate sanitizerRouteSameLevelFlowReachable( + string modulePath, string sourceRef, string sinkRef +) { + sanitizerRouteNonCallFlowStep(modulePath, sourceRef, sinkRef) + or + exists(string middleRef | + sanitizerRouteNonCallFlowStep(modulePath, sourceRef, middleRef) and + sanitizerRouteSameLevelFlowReachable(modulePath, middleRef, sinkRef) + ) +} + +private predicate sanitizerResultSameLevelFlowStep( + string modulePath, string sourceRef, string sinkRef +) { + sanitizerRouteNonCallFlowStep(modulePath, sourceRef, sinkRef) + or + exists(LuaInterproceduralFlow argumentFlow, LuaInterproceduralFlow returnFlow | + pairedCallFlows(argumentFlow, returnFlow) and + argumentFlow.getCallerModulePath() = modulePath and + argumentFlow.getSourceRef() = sourceRef and + returnFlow.getSinkRef() = sinkRef and + ( + argumentFlow.getSinkRef() = returnFlow.getSourceRef() or + sanitizerResultSameLevelFlowReachable(argumentFlow.getCalleeModulePath(), + argumentFlow.getSinkRef(), returnFlow.getSourceRef()) + ) + ) +} + +private predicate sanitizerResultSameLevelFlowReachable( + string modulePath, string sourceRef, string sinkRef +) { + sanitizerResultSameLevelFlowStep(modulePath, sourceRef, sinkRef) + or + exists(string middleRef | + sanitizerResultSameLevelFlowStep(modulePath, sourceRef, middleRef) and + sanitizerResultSameLevelFlowReachable(modulePath, middleRef, sinkRef) + ) +} + +private predicate sanitizerDownwardFlowReachable( + string sourceModule, string sourceRef, string sinkModule, string sinkRef +) { + sourceModule = sinkModule and + sanitizerRouteSameLevelFlowReachable(sourceModule, sourceRef, sinkRef) + or + exists(LuaInterproceduralFlow argumentFlow | + ( + argumentFlow.getFlowKind() = "argument-to-parameter" or + argumentFlow.getFlowKind() = "argument-to-vararg" + ) and + argumentFlow.getCallerModulePath() = sourceModule and + ( + sourceRef = argumentFlow.getSourceRef() or + sanitizerRouteSameLevelFlowReachable(sourceModule, sourceRef, argumentFlow.getSourceRef()) + ) and + ( + argumentFlow.getCalleeModulePath() = sinkModule and + argumentFlow.getSinkRef() = sinkRef + or + sanitizerDownwardFlowReachable(argumentFlow.getCalleeModulePath(), argumentFlow.getSinkRef(), + sinkModule, sinkRef) + ) + ) +} + +private predicate unsanitizedRouteUsesCallEdge( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, + LuaInterproceduralFlow routeEdge +) { + exists(LuaInterproceduralFlow activeEdge | + ( + activeEdge.getFlowKind() = "argument-to-parameter" or + activeEdge.getFlowKind() = "argument-to-vararg" + ) and + activeEdge.getCallerModulePath() = routeEdge.getCallerModulePath() and + activeEdge.getCallerPrototypeId() = routeEdge.getCallerPrototypeId() and + activeEdge.getCalleeModulePath() = routeEdge.getCalleeModulePath() and + activeEdge.getCalleePrototypeId() = routeEdge.getCalleePrototypeId() and + unsanitizedDownwardFlowReachable(sourceModule, sourceRef, activeEdge.getCallerModulePath(), + activeEdge.getSourceRef()) and + unsanitizedDownwardFlowReachable(activeEdge.getCalleeModulePath(), activeEdge.getSinkRef(), + sinkModule, sinkRef) + ) +} + +private predicate unsanitizedRouteUsesReturnEdge( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, + LuaInterproceduralFlow routeEdge +) { + exists(LuaInterproceduralFlow activeEdge | + activeEdge.getFlowKind() = "return-to-result" and + activeEdge.getCallerModulePath() = routeEdge.getCallerModulePath() and + activeEdge.getCallerPrototypeId() = routeEdge.getCallerPrototypeId() and + activeEdge.getCalleeModulePath() = routeEdge.getCalleeModulePath() and + activeEdge.getCalleePrototypeId() = routeEdge.getCalleePrototypeId() and + unsanitizedDownwardFlowReachable(sourceModule, sourceRef, activeEdge.getCalleeModulePath(), + activeEdge.getSourceRef()) and + unsanitizedDownwardFlowReachable(activeEdge.getCallerModulePath(), activeEdge.getSinkRef(), + sinkModule, sinkRef) + ) +} + +bindingset[sourceModule, sourceRef, sinkModule, sinkRef] +private predicate guardSanitizerOnPath( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, string sanitizerModule, + string sanitizerCallsiteId, string sanitizerName +) { + exists(LuaCallSite sanitizer, LuaRegisterEvent argumentRead, string sanitizedValueRef | + sanitizerCall(sanitizerModule, sanitizerCallsiteId, sanitizerName, sanitizedValueRef, _) and + not sanitizerResultOnChain(sourceModule, sourceRef, sanitizerModule, sanitizedValueRef, + sinkModule, sinkRef) and + sanitizer.getFixtureId() = sanitizerModule and + sanitizer.getCallsiteId() = sanitizerCallsiteId and + sanitizerArgumentRead(sanitizer, argumentRead) and + genericFlowReachable(sourceModule, sourceRef, sanitizerModule, argumentRead.getValueRef()) and + ( + exists(LuaRegisterEvent sinkRead | + sinkRead.getValueRef() = sinkRef and + sinkRead.isRead() and + sinkRead.getInstruction().getFixtureId() = sinkModule and + instructionDominates(sanitizer.getInstruction(), sinkRead.getInstruction()) + ) + or + exists( + LuaInterproceduralFlow outgoingFlow, LuaCallSite outgoingCall, LuaRegisterEvent outgoingRead + | + ( + outgoingFlow.getFlowKind() = "argument-to-parameter" or + outgoingFlow.getFlowKind() = "argument-to-vararg" + ) and + outgoingFlow.getCallerModulePath() = sanitizerModule and + outgoingCall.getFixtureId() = outgoingFlow.getCallerModulePath() and + outgoingCall.getPrototypeId() = outgoingFlow.getCallerPrototypeId() and + outgoingCall.getCallsiteId() = outgoingFlow.getCallsiteId() and + outgoingRead.getInstruction() = outgoingCall.getInstruction() and + outgoingRead.isRead() and + outgoingRead.getValueRef() = outgoingFlow.getSourceRef() and + genericFlowReachable(sourceModule, sourceRef, sanitizerModule, outgoingRead.getValueRef()) and + instructionDominates(sanitizer.getInstruction(), outgoingCall.getInstruction()) and + ( + outgoingFlow.getCalleeModulePath() = sinkModule and + outgoingFlow.getSinkRef() = sinkRef + or + downwardCallFlowReachable(outgoingFlow.getCalleeModulePath(), outgoingFlow.getSinkRef(), + sinkModule, sinkRef) + ) + ) + ) + ) +} + +private predicate sanitizedClassification( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, string sanitizerModule, + string sanitizerCallsiteId, string sanitizerName +) { + sourceEndpoint(sourceModule, sourceRef, _, _, _) and + sinkEndpoint(sinkModule, sinkRef, _, _, _, _) and + genericFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) and + exists(string sanitizedValueRef | + sanitizerCall(sanitizerModule, sanitizerCallsiteId, sanitizerName, sanitizedValueRef, _) and + ( + guardSanitizerOnPath(sourceModule, sourceRef, sinkModule, sinkRef, sanitizerModule, + sanitizerCallsiteId, sanitizerName) + or + sanitizerResultOnChain(sourceModule, sourceRef, sanitizerModule, sanitizedValueRef, + sinkModule, sinkRef) + ) and + ( + not unsanitizedFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) + or + sanitizerResultOnIndependentCallRoute(sourceModule, sourceRef, sanitizerModule, + sanitizerCallsiteId, sanitizedValueRef, sinkModule, sinkRef) + ) + ) +} + +predicate sanitizedReportPath(LuaFlowNode source, LuaFlowNode sink) { + sanitizedClassification(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef(), _, _, _) +} + +private predicate notSanitizedClassification( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, string sanitizerModule, + string sanitizerCallsiteId, string sanitizerName, string appliesToSink, string onDataflowChain +) { + sourceEndpoint(sourceModule, sourceRef, _, _, _) and + sinkEndpoint(sinkModule, sinkRef, _, _, _, _) and + genericFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) and + exists(string sanitizedValueRef | + sanitizerCall(sanitizerModule, sanitizerCallsiteId, sanitizerName, sanitizedValueRef, _) and + ( + sanitizerModule = sourceModule and + not guardSanitizerOnPath(sourceModule, sourceRef, sinkModule, sinkRef, sanitizerModule, + sanitizerCallsiteId, sanitizerName) and + not sanitizerResultOnChain(sourceModule, sourceRef, sanitizerModule, sanitizedValueRef, + sinkModule, sinkRef) and + appliesToSink = "false" and + onDataflowChain = "false" + or + sanitizerResultOnChain(sourceModule, sourceRef, sanitizerModule, sanitizedValueRef, + sinkModule, sinkRef) and + unsanitizedFlowReachable(sourceModule, sourceRef, sinkModule, sinkRef) and + appliesToSink = "true" and + onDataflowChain = "true" + ) + ) +} + +predicate sanitizerClassification( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, string sanitizerModule, + string sanitizerCallsiteId, string sanitizerName, string appliesToSink, string onDataflowChain, + string classification +) { + sanitizedClassification(sourceModule, sourceRef, sinkModule, sinkRef, sanitizerModule, + sanitizerCallsiteId, sanitizerName) and + appliesToSink = "true" and + onDataflowChain = "true" and + classification = "sanitized" + or + notSanitizedClassification(sourceModule, sourceRef, sinkModule, sinkRef, sanitizerModule, + sanitizerCallsiteId, sanitizerName, appliesToSink, onDataflowChain) and + classification = "not-sanitized" +} + +predicate reportClassification( + string sourceModule, string sourceRef, string sinkModule, string sinkRef, string classification, + string reason +) { + sanitizedClassification(sourceModule, sourceRef, sinkModule, sinkRef, _, _, _) and + classification = "sanitized" and + reason = "sanitized path suppressed" + or + exists(LuaFlowNode source, LuaFlowNode sink, string provenance | + source.getModulePath() = sourceModule and + source.getValueRef() = sourceRef and + sink.getModulePath() = sinkModule and + sink.getValueRef() = sinkRef and + activeReportPath(source, sink, classification, reason, provenance) + ) +} diff --git a/lua/ql/lib/codeql/lua/SourceFile.qll b/lua/ql/lib/codeql/lua/SourceFile.qll new file mode 100644 index 000000000000..dba6d07c2b25 --- /dev/null +++ b/lua/ql/lib/codeql/lua/SourceFile.qll @@ -0,0 +1,16 @@ +/** + * Provides source-file inventory facts for Lua inputs. + */ +class LuaSourceFile extends @lua_source_file { + string getPath() { lua_source_files(this, _, result, _, _, _) } + + int getLineCount() { lua_source_files(this, _, _, result, _, _) } + + int getByteCount() { lua_source_files(this, _, _, _, result, _) } + + string getSha256() { lua_source_files(this, _, _, _, _, result) } + + string getBaseName() { result = this.getPath().regexpReplaceAll(".*/", "") } + + string toString() { result = this.getPath() } +} diff --git a/lua/ql/lib/lua.dbscheme b/lua/ql/lib/lua.dbscheme new file mode 100644 index 000000000000..e66aad4742fa --- /dev/null +++ b/lua/ql/lib/lua.dbscheme @@ -0,0 +1,315 @@ +files( + unique int id: @file, + string name: string ref +); + +folders( + unique int id: @folder, + string name: string ref +); + +@container = @file | @folder + +containerparent( + int parent: @container ref, + unique int child: @container ref +); + +locations_default( + unique int id: @location_default, + int file: @file ref, + int beginLine: int ref, + int beginColumn: int ref, + int endLine: int ref, + int endColumn: int ref +); + +sourceLocationPrefix( + string prefix: string ref +); + +lua_source_files( + unique int id: @lua_source_file, + int file: @file ref, + string path: string ref, + int line_count: int ref, + int byte_count: int ref, + string sha256: string ref +); + +lua_artifacts( + unique int id: @lua_artifact, + string fixture_id: string ref, + string path: string ref, + string input_kind: string ref, + string profile_id: string ref, + int accepted: int ref, + string provenance: string ref +); + +lua_profiles( + unique int artifact: @lua_artifact ref, + string version: string ref, + int format: int ref, + int little_endian: int ref, + int int_size: int ref, + int size_t_size: int ref, + int instruction_size: int ref, + int lua_number_size: int ref, + int integral_flag: int ref +); + +lua_prototypes( + unique int id: @lua_prototype, + int artifact: @lua_artifact ref, + string fixture_id: string ref, + string prototype_id: string ref, + string parent_prototype_id: string ref, + int ordinal_index: int ref, + int num_params: int ref, + int is_vararg: int ref, + int max_stack: int ref, + int upvalue_count: int ref, + string debug_name: string ref, + string mapping_state: string ref, + string provenance: string ref +); + +lua_instructions( + unique int id: @lua_instruction, + int prototype: @lua_prototype ref, + string fixture_id: string ref, + string prototype_id: string ref, + int pc: int ref, + string opcode: string ref, + int operand_a: int ref, + int operand_b: int ref, + int operand_c: int ref +); + +lua_constants( + unique int id: @lua_constant, + int prototype: @lua_prototype ref, + string fixture_id: string ref, + string constant_id: string ref, + string prototype_id: string ref, + int index: int ref, + string lua_type: string ref, + string value: string ref +); + +lua_register_events( + unique int id: @lua_register_event, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string prototype_id: string ref, + int pc: int ref, + string event_kind: string ref, + int slot_index: int ref, + string value_ref: string ref +); + +lua_semantic_steps( + unique int id: @lua_semantic_step, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string source_ref: string ref, + string dest_ref: string ref, + string step_kind: string ref +); + +lua_closure_values( + unique int id: @lua_closure_value, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string value_ref: string ref, + string target_prototype_id: string ref, + string provenance: string ref +); + +lua_call_sites( + unique int id: @lua_call_site, + int instruction: @lua_instruction ref, + string fixture_id: string ref, + string callsite_id: string ref, + string prototype_id: string ref, + int pc: int ref, + string opcode: string ref, + string target_value_ref: string ref, + int first_arg_slot: int ref, + int arg_count: int ref, + int first_return_slot: int ref, + int return_count: int ref +); + +lua_upvalues( + unique int id: @lua_upvalue, + int prototype: @lua_prototype ref, + string fixture_id: string ref, + string upvalue_id: string ref, + string prototype_id: string ref, + int upvalue_index: int ref, + string debug_name: string ref, + string mapping_state: string ref, + string provenance: string ref +); + +lua_local_flows( + unique int id: @lua_local_flow, + string module_path: string ref, + string prototype_id: string ref, + string source_ref: string ref, + string sink_ref: string ref, + string edge_kind: string ref, + string provenance: string ref +); + +lua_control_flow_edges( + unique int id: @lua_control_flow_edge, + int source_instruction: @lua_instruction ref, + int target_instruction: @lua_instruction ref, + string module_path: string ref, + string prototype_id: string ref, + int source_pc: int ref, + int target_pc: int ref, + string provenance: string ref +); + +lua_dominator_tree_intervals( + unique int id: @lua_dominator_tree_interval, + int instruction: @lua_instruction ref, + string module_path: string ref, + string prototype_id: string ref, + int pc: int ref, + int start: int ref, + int end: int ref, + string provenance: string ref +); + +lua_analysis_boundaries( + unique int id: @lua_analysis_boundary, + string module_path: string ref, + string prototype_id: string ref, + string site_id: string ref, + string boundary_kind: string ref, + string reason: string ref, + string provenance: string ref +); + +lua_table_field_flows( + unique int id: @lua_table_field_flow, + string module_path: string ref, + string prototype_id: string ref, + string table_ref: string ref, + string field_name: string ref, + string write_ref: string ref, + string read_ref: string ref, + string provenance: string ref +); + +lua_global_flows( + unique int id: @lua_global_flow, + string fixture_id: string ref, + string global_name: string ref, + string write_ref: string ref, + string read_ref: string ref, + string value_ref: string ref, + string provenance: string ref +); + +lua_upvalue_flows( + unique int id: @lua_upvalue_flow, + string fixture_id: string ref, + string upvalue_id: string ref, + string capture_ref: string ref, + string read_ref: string ref, + string write_ref: string ref, + string provenance: string ref +); + +lua_call_resolutions( + unique int id: @lua_call_resolution, + string caller_module_path: string ref, + string caller_prototype_id: string ref, + string callsite_id: string ref, + string target_value_ref: string ref, + string resolved_name: string ref, + string resolution_kind: string ref, + string target_module_path: string ref, + string target_prototype_id: string ref, + string provenance: string ref +); + +lua_literal_requires( + unique int id: @lua_literal_require, + string caller_module_path: string ref, + string caller_prototype_id: string ref, + string callsite_id: string ref, + string require_string: string ref, + string argument_ref: string ref, + string provenance: string ref +); + +lua_module_resolutions( + unique int id: @lua_module_resolution, + string caller_module_path: string ref, + string callsite_id: string ref, + string require_string: string ref, + string status: string ref, + string target_module_path: string ref, + string reason: string ref, + string provenance: string ref +); + +lua_module_exports( + unique int id: @lua_module_export, + string module_path: string ref, + string export_kind: string ref, + string field_name: string ref, + string value_ref: string ref, + string target_prototype_id: string ref, + string provenance: string ref +); + +lua_interprocedural_flows( + unique int id: @lua_interprocedural_flow, + string caller_module_path: string ref, + string caller_prototype_id: string ref, + string callsite_id: string ref, + string callee_module_path: string ref, + string callee_prototype_id: string ref, + string source_ref: string ref, + string sink_ref: string ref, + string flow_kind: string ref, + int position: int ref, + string provenance: string ref +); + +lua_diagnostics( + unique int id: @lua_diagnostic, + int artifact: @lua_artifact ref, + string fixture_id: string ref, + string diagnostic_id: string ref, + string kind: string ref, + string input_ref: string ref, + string severity: string ref, + string message_category: string ref, + int success_facts_allowed: int ref, + string provenance: string ref +); + +lua_mapping_markers( + unique int id: @lua_mapping_marker, + string fixture_id: string ref, + string mapping_kind: string ref, + string bytecode_ref: string ref, + string state: string ref, + string provenance: string ref +); + +lua_mapping_marker_diagnostics( + unique int id: @lua_mapping_marker_diagnostic, + int marker: @lua_mapping_marker ref, + string fixture_id: string ref, + string diagnostic_kind: string ref +); diff --git a/lua/ql/lib/lua.dbscheme.stats b/lua/ql/lib/lua.dbscheme.stats new file mode 100644 index 000000000000..02d970bb5b5f --- /dev/null +++ b/lua/ql/lib/lua.dbscheme.stats @@ -0,0 +1,109 @@ + + + + @file + 0 + + + @folder + 0 + + + @location_default + 0 + + + @lua_source_file + 0 + + + @lua_artifact + 0 + + + @lua_prototype + 0 + + + @lua_instruction + 0 + + + @lua_constant + 0 + + + @lua_register_event + 0 + + + @lua_semantic_step + 0 + + + @lua_closure_value + 0 + + + @lua_call_site + 0 + + + @lua_upvalue + 0 + + + @lua_local_flow + 0 + + + @lua_analysis_boundary + 0 + + + @lua_table_field_flow + 0 + + + @lua_global_flow + 0 + + + @lua_upvalue_flow + 0 + + + @lua_call_resolution + 0 + + + @lua_literal_require + 0 + + + @lua_module_resolution + 0 + + + @lua_module_export + 0 + + + @lua_interprocedural_flow + 0 + + + @lua_diagnostic + 0 + + + @lua_mapping_marker + 0 + + + @lua_mapping_marker_diagnostic + 0 + + + + diff --git a/lua/ql/lib/lua.qll b/lua/ql/lib/lua.qll new file mode 100644 index 000000000000..a72dabee77cd --- /dev/null +++ b/lua/ql/lib/lua.qll @@ -0,0 +1,9 @@ +/** + * Provides the public Lua 5.1 bytecode analysis libraries. + */ + +import codeql.lua.SourceFile +import codeql.lua.Bytecode +import codeql.lua.IntraproceduralSemantics +import codeql.lua.InterproceduralModuleTaint +import codeql.lua.RulesSanitizerReport diff --git a/lua/ql/lib/qlpack.yml b/lua/ql/lib/qlpack.yml new file mode 100644 index 000000000000..98a3786140f1 --- /dev/null +++ b/lua/ql/lib/qlpack.yml @@ -0,0 +1,9 @@ +name: codeql/lua-all +version: 0.1.0-dev +groups: lua +library: true +extractor: lua +dbscheme: lua.dbscheme +dependencies: + codeql/util: ${workspace} +warnOnImplicitThis: true diff --git a/lua/ql/src/README.md b/lua/ql/src/README.md new file mode 100644 index 000000000000..d4bd7a446669 --- /dev/null +++ b/lua/ql/src/README.md @@ -0,0 +1,93 @@ +# Lua query interfaces + +The Lua query pack exposes one security query, one diagnostic path query, and +one detailed table query. These queries use CodeQL's experimental support +lifecycle for initial upstream adoption. The Lua 5.1 extractor, schema, and +analysis libraries are mature and are not designated experimental. + +See [`../../README.md`](../../README.md) for bytecode and source workflows, +database creation, suite execution, and prerequisites. + +## `CommandInjection.ql` + +Location: `lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql` + +This is the active-finding interface over `RulesSanitizerReport.qll`. A decoded +BQRS row has the standard path-problem projection: + +| Column | Meaning | +| --- | --- | +| `file` | Sink module path relative to the database source root. | +| `source` | Typed `LuaFlowNode` for the matched source value. | +| `sink` | Typed `LuaFlowNode` for the matched sink value. | +| `col3` | Human-readable classification, reason, and provenance message. | + +Its `edges` predicate supplies complete native flow steps for BQRS +interpretation as SARIF. Use this query when a reviewer or adapter needs only +reportable active findings. Source rules currently match final-segment +`formvalue` and `source`; sink rules cover the committed command-execution call +family defined in `RulesSanitizerReport.qll`. The rules use resolved call names +and semantic flow through the generic Lua bytecode model. + +## `SanitizedCommandFlow.ql` + +Location: `lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql` + +This companion path-problem query uses the same `file`, `source`, `sink`, and +`col3` projection, but emits flows classified as `sanitized`. It is review +evidence for suppressed paths, not an active security finding. Its SARIF rule +ID is `lua/diagnostics/sanitized-command-flow`. + +## `LuaBytecodeFacts.ql` + +Location: `lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql` + +This query exports a tagged union with columns `row_kind`, `a` through `i`. +`row_kind` determines the meaning of the remaining columns: + +- aggregate model counts; +- instructions, constants, upvalues, closures, and register events; +- local dataflow edges and call-target candidates; +- module identities, literal requires, resolution, and returned exports; +- interprocedural argument/return flow; +- source/sink rule matches. + +The current row kinds are `aggregate`, `instruction`, `constant`, `upvalue`, +`closure-value`, `register-event`, `dataflow-edge`, `module-identity`, +`function-identity-candidate`, `call-target-candidate`, `literal-require`, +`module-resolution`, `module-export`, `module-field-call-target`, +`interproc-arg-flow`, `interproc-return-flow`, and `rule-match`. The query +source is the positional schema authority. Empty columns are intentionally +emitted as empty strings so every row has ten columns. + +Stable identity conventions are documented in [`../../SCHEMA.md`](../../SCHEMA.md). + +Use this interface when an integration needs auditable model evidence. It is +deliberately verbose and is intended for direct, manual execution. + +## Execute either query + +After creating a Lua database as described in `lua/README.md`: + +```bash +CODEQL=/absolute/path/to/codeql +ROOT=$(pwd) +DB=/absolute/path/to/codeql-lua-database +QUERY="$ROOT/lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql" + +"$CODEQL" query run "$QUERY" \ + --database="$DB" \ + --search-path="$ROOT:$ROOT/lua" \ + --output=/tmp/lua-results.bqrs + +"$CODEQL" bqrs decode /tmp/lua-results.bqrs \ + --format=csv \ + --output=/tmp/lua-results.csv +``` + +Replace `QUERY` with +`$ROOT/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql` for +suppressed paths, or +`$ROOT/lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql` for the detailed +export. Use `--format=json` for the facts export when a consumer needs to +preserve the tagged row structure without CSV quoting concerns. diff --git a/lua/ql/src/change-notes/2026-07-21-lua-bytecode-queries.md b/lua/ql/src/change-notes/2026-07-21-lua-bytecode-queries.md new file mode 100644 index 000000000000..5f6e45b486a2 --- /dev/null +++ b/lua/ql/src/change-notes/2026-07-21-lua-bytecode-queries.md @@ -0,0 +1,6 @@ +--- +category: newQuery +--- +* Added the experimental `lua/command-injection`, + `lua/diagnostics/sanitized-command-flow`, and + `lua/diagnostics/bytecode-facts` queries for Lua 5.1 bytecode. diff --git a/lua/ql/src/codeql-suites/lua-code-quality-extended.qls b/lua/ql/src/codeql-suites/lua-code-quality-extended.qls new file mode 100644 index 000000000000..c6ce6677e288 --- /dev/null +++ b/lua/ql/src/codeql-suites/lua-code-quality-extended.qls @@ -0,0 +1,4 @@ +- description: Extended code-quality queries for Lua +- queries: . +- apply: code-quality-extended-selectors.yml + from: codeql/suite-helpers diff --git a/lua/ql/src/codeql-suites/lua-code-quality.qls b/lua/ql/src/codeql-suites/lua-code-quality.qls new file mode 100644 index 000000000000..7719b2208591 --- /dev/null +++ b/lua/ql/src/codeql-suites/lua-code-quality.qls @@ -0,0 +1,4 @@ +- description: Code-quality queries for Lua +- queries: . +- apply: code-quality-selectors.yml + from: codeql/suite-helpers diff --git a/lua/ql/src/codeql-suites/lua-code-scanning.qls b/lua/ql/src/codeql-suites/lua-code-scanning.qls new file mode 100644 index 000000000000..58b0ff7e5462 --- /dev/null +++ b/lua/ql/src/codeql-suites/lua-code-scanning.qls @@ -0,0 +1,4 @@ +- description: Standard Code Scanning queries for Lua +- queries: . +- apply: code-scanning-selectors.yml + from: codeql/suite-helpers diff --git a/lua/ql/src/codeql-suites/lua-security-and-quality.qls b/lua/ql/src/codeql-suites/lua-security-and-quality.qls new file mode 100644 index 000000000000..3eca70343b93 --- /dev/null +++ b/lua/ql/src/codeql-suites/lua-security-and-quality.qls @@ -0,0 +1,4 @@ +- description: Security-and-quality queries for Lua +- queries: . +- apply: security-and-quality-selectors.yml + from: codeql/suite-helpers diff --git a/lua/ql/src/codeql-suites/lua-security-experimental.qls b/lua/ql/src/codeql-suites/lua-security-experimental.qls new file mode 100644 index 000000000000..868d3396f5b0 --- /dev/null +++ b/lua/ql/src/codeql-suites/lua-security-experimental.qls @@ -0,0 +1,4 @@ +- description: Extended and experimental security queries for Lua +- queries: . +- apply: security-experimental-selectors.yml + from: codeql/suite-helpers diff --git a/lua/ql/src/codeql-suites/lua-security-extended.qls b/lua/ql/src/codeql-suites/lua-security-extended.qls new file mode 100644 index 000000000000..0e36e0de858e --- /dev/null +++ b/lua/ql/src/codeql-suites/lua-security-extended.qls @@ -0,0 +1,4 @@ +- description: Security-extended queries for Lua +- queries: . +- apply: security-extended-selectors.yml + from: codeql/suite-helpers diff --git a/lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql b/lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql new file mode 100644 index 000000000000..fa6179058241 --- /dev/null +++ b/lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql @@ -0,0 +1,281 @@ +/** + * @name Lua bytecode fact export + * @description Exports detailed Lua bytecode, flow, and rule facts for integration. + * @kind table + * @id lua/diagnostics/bytecode-facts + * @tags experimental + */ + +import codeql.lua.Bytecode +import codeql.lua.IntraproceduralSemantics +import codeql.lua.InterproceduralModuleTaint +import codeql.lua.RulesSanitizerReport + +from + string row_kind, string a, string b, string c, string d, string e, string f, string g, string h, + string i +where + row_kind = "aggregate" and + a = "accepted-artifacts" and + b = count(LuaArtifact artifact | artifact.isAccepted()).toString() and + c = "" and + d = "" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + row_kind = "aggregate" and + a = "diagnostic-artifacts" and + b = count(LuaArtifact artifact | not artifact.isAccepted()).toString() and + c = "" and + d = "" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + row_kind = "aggregate" and + a = "prototypes" and + b = count(LuaPrototype prototype | any()).toString() and + c = "" and + d = "" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + row_kind = "aggregate" and + a = "instructions" and + b = count(LuaInstruction instruction | any()).toString() and + c = "" and + d = "" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + row_kind = "aggregate" and + a = "constants" and + b = count(LuaConstant constant | any()).toString() and + c = "" and + d = "" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + row_kind = "aggregate" and + a = "callsites" and + b = count(LuaCallSite callsite | any()).toString() and + c = "" and + d = "" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + exists(LuaInstruction instr | + instr.getFixtureId() = a and + instr.getPrototype().getArtifact().isAccepted() and + row_kind = "instruction" and + b = instr.getPrototype().getArtifact().getPath() and + c = instr.getPrototypeId() and + d = instr.getPc().toString() and + e = instr.getOpcode() and + f = instr.getOperandA().toString() and + g = instr.getOperandB().toString() and + h = instr.getOperandC().toString() and + i = "bytecode-only,instruction" + ) + or + exists(LuaConstant constant | + constant.getFixtureId() = a and + constant.getPrototype().getArtifact().isAccepted() and + row_kind = "constant" and + b = constant.getPrototype().getArtifact().getPath() and + c = constant.getPrototypeId() and + d = constant.getIndex().toString() and + e = constant.getLuaType() and + f = constant.getValue() and + g = constant.getConstantId() and + h = "bytecode-only,constant" and + i = "" + ) + or + exists(LuaUpvalue upvalue | + upvalue.getFixtureId() = a and + upvalue.getPrototype().getArtifact().isAccepted() and + row_kind = "upvalue" and + b = upvalue.getPrototype().getArtifact().getPath() and + c = upvalue.getPrototypeId() and + d = upvalue.getIndex().toString() and + e = upvalue.getDebugName() and + f = upvalue.getMappingState() and + g = upvalue.getUpvalueId() and + h = upvalue.getProvenance() and + i = "" + ) + or + exists(LuaClosureValue closure | + closure.getFixtureId() = a and + closure.getInstruction().getPrototype().getArtifact().isAccepted() and + row_kind = "closure-value" and + b = closure.getInstruction().getPrototype().getArtifact().getPath() and + c = closure.getInstruction().getPrototypeId() and + d = closure.getValueRef() and + e = closure.getTargetPrototypeId() and + f = closure.getProvenance() and + g = "" and + h = "" and + i = "" + ) + or + exists(LuaRegisterEvent event | + event.getFixtureId() = a and + event.getInstruction().getPrototype().getArtifact().isAccepted() and + row_kind = "register-event" and + b = event.getPrototypeId() and + c = event.getInstruction().getPrototype().getArtifact().getPath() and + d = event.getPc().toString() and + e = event.getKind() and + f = event.getSlot().toString() and + g = event.getValueRef() and + h = "bytecode-only,register-event" and + i = "" + ) + or + exists(LuaSemanticStep step | + step.getFixtureId() = a and + step.getInstruction().getPrototype().getArtifact().isAccepted() and + row_kind = "dataflow-edge" and + b = step.getInstruction().getPrototype().getArtifact().getPath() and + c = step.getInstruction().getPrototypeId() and + d = step.getSourceRef() and + e = step.getDestRef() and + f = step.getKind() and + g = "bytecode-only,semantic-step" and + h = step.getInstruction().getPc().toString() and + i = step.getInstruction().getPc().toString() + ) + or + moduleIdentity(a, b, c, d) and + row_kind = "module-identity" and + e = "" and + f = "" and + g = "" and + h = "" and + i = "" + or + exists(LuaPrototype prototype | + prototype.getFixtureId() = a and + prototype.getArtifact().isAccepted() and + row_kind = "function-identity-candidate" and + b = prototype.getArtifact().getPath() and + c = prototype.getPrototypeId() and + d = prototype.getPrototypeId() and + e = "bytecode-prototype-id" and + f = "bytecode-only,prototype-identity" and + g = "" and + h = "" and + i = "" + ) + or + exists(LuaCallResolution resolution | + resolution.getCallerModulePath() = a and + e = resolution.getTargetValueRef() and + row_kind = "call-target-candidate" and + exists(LuaCallSite call | + call.getFixtureId() = resolution.getCallerModulePath() and + call.getCallsiteId() = resolution.getCallsiteId() and + call.getInstruction().getPrototype().getArtifact().isAccepted() and + b = call.getInstruction().getPrototype().getArtifact().getPath() and + c = resolution.getCallerPrototypeId() and + d = resolution.getCallsiteId() and + i = call.getPc().toString() + ) and + ( + resolution.getResolvedName() != "" and f = resolution.getResolvedName() + or + resolution.getResolvedName() = "" and f = resolution.getTargetPrototypeId() + ) and + g = resolution.getResolutionKind() and + h = resolution.getProvenance() + ) + or + literalRequireCall(a, b, c, any(int pc), d, e, f, g) and + row_kind = "literal-require" and + h = "" and + i = "" + or + moduleResolution(a, b, c, d, e, f, g, h) and + row_kind = "module-resolution" and + i = "" + or + moduleExport(a, b, c, d, e, f, g) and + row_kind = "module-export" and + h = "" and + i = "" + or + exists(string fieldCallsite, string provenance | + moduleFieldCallTarget(a, d, fieldCallsite, f, e, g, provenance) and + exists(LuaModuleResolution resolution | + resolution.getCallerModulePath() = a and + resolution.getStatus() = "matched" and + resolution.getTargetModulePath() = e and + b = resolution.getCallsiteId() and + c = resolution.getRequireString() and + exists(LuaCallResolution callResolution | + callResolution.getCallerModulePath() = a and + callResolution.getCallsiteId() = fieldCallsite and + callResolution.getResolvedName() = c + "." + f + ) + ) and + row_kind = "module-field-call-target" and + h = provenance and + i = fieldCallsite + ) + or + interproceduralArgFlow(a, b, e, h, d, f, g) and + row_kind = "interproc-arg-flow" and + exists(string callerPrototype | + c = callerPrototype and + exists(LuaCallSite call | + call.getFixtureId() = a and + call.getCallsiteId() = b and + callerPrototype = call.getPrototypeId() + ) + ) and + i = "" + or + interproceduralReturnFlow(a, b, h, d, e, f, g) and + row_kind = "interproc-return-flow" and + exists(string callerPrototype | + c = callerPrototype and + exists(LuaCallSite call | + call.getFixtureId() = a and + call.getCallsiteId() = b and + callerPrototype = call.getPrototypeId() + ) + ) and + i = "" + or + exists(int parameterIndex | + sourceSinkRuleMatch(a, d, e, f, g, parameterIndex, h) and + row_kind = "rule-match" and + exists(LuaCallSite call | + call.getFixtureId() = a and + call.getCallsiteId() = d and + call.getInstruction().getPrototype().getArtifact().isAccepted() and + b = call.getInstruction().getPrototype().getArtifact().getPath() and + c = call.getPrototypeId() and + i = call.getPc().toString() + ":" + parameterIndex.toString() + ) + ) +select row_kind, a, b, c, d, e, f, g, h, i diff --git a/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.qhelp b/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.qhelp new file mode 100644 index 000000000000..4af7e28238ae --- /dev/null +++ b/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.qhelp @@ -0,0 +1,38 @@ + + + + +

This diagnostic shows Lua 5.1 bytecode paths from external data to command-execution APIs when +a recognized sanitizer suppresses the corresponding security finding.

+ +

The result is review information, not a security alert. It records path-local sanitizer +behavior so reviewers can confirm that the sanitizer applies to the value reaching the sink.

+ +
+ + +

Review the reported path and confirm that the sanitizer is appropriate for the command context. +Do not treat sanitization on an unrelated path or value as protection for the reported sink.

+ +

Lua source must first be compiled with luac5.1, preserving its relative file layout, +because the analysis operates on Lua 5.1 bytecode.

+ +
+ + +

The following example applies a shell sanitizer to the value that reaches the command API.

+ + + +
+ + +
  • +OWASP: +Command Injection. +
  • + +
    +
    diff --git a/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql b/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql new file mode 100644 index 000000000000..73001c75b0e0 --- /dev/null +++ b/lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql @@ -0,0 +1,61 @@ +/** + * @name Sanitized command flow + * @description Shows Lua command flows that are suppressed by a sanitizer on the path. + * @kind path-problem + * @problem.severity recommendation + * @precision high + * @id lua/diagnostics/sanitized-command-flow + * @tags experimental + */ + +import codeql.lua.RulesSanitizerReport + +class LuaSanitizedPathFile extends @file { + LuaSanitizedPathFile() { files(this, _) } + + string getPath() { files(this, result) } + + string getURL() { + exists(string prefix | + sourceLocationPrefix(prefix) and + result = "file://" + prefix + "/" + this.getPath() + ":0:0:0:0" + ) + } + + string toString() { result = this.getPath() } +} + +private predicate sanitizedPath(LuaFlowNode source, LuaFlowNode sink) { + sanitizedReportPath(source, sink) +} + +bindingset[sink] +bindingset[source] +private predicate reachesOrEquals(LuaFlowNode source, LuaFlowNode sink) { + source = sink + or + genericFlowReachable(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef()) +} + +query predicate edges(LuaFlowNode source, LuaFlowNode sink) { + genericFlowStep(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef(), _, _) and + exists(LuaFlowNode pathSource, LuaFlowNode pathSink | + sanitizedPath(pathSource, pathSink) and + reachesOrEquals(pathSource, source) and + reachesOrEquals(sink, pathSink) + ) +} + +from + LuaSanitizedPathFile file, LuaFlowNode source, LuaFlowNode sink, string classification, + string reason +where + sanitizedPath(source, sink) and + classification = "sanitized" and + reason = "sanitized path suppressed" and + file.getPath() = sink.getModulePath() +select file, source, sink, + "Sanitized Lua bytecode flow from " + source.toString() + " reaches this sink. Classification: " + + classification + "; reason: " + reason + "." diff --git a/lua/ql/src/experimental/Diagnostics/examples/SanitizedCommandFlow.lua b/lua/ql/src/experimental/Diagnostics/examples/SanitizedCommandFlow.lua new file mode 100644 index 000000000000..1d58d042902f --- /dev/null +++ b/lua/ql/src/experimental/Diagnostics/examples/SanitizedCommandFlow.lua @@ -0,0 +1,4 @@ +local host = luci.http.formvalue("host") +local safe_host = shellquote(host) + +os.execute("ping -c 1 " .. safe_host) diff --git a/lua/ql/src/experimental/README.md b/lua/ql/src/experimental/README.md new file mode 100644 index 000000000000..1b0673404010 --- /dev/null +++ b/lua/ql/src/experimental/README.md @@ -0,0 +1,5 @@ +# Experimental Lua queries + +This directory contains Lua queries in CodeQL's experimental support +lifecycle. The designation applies to query publication and upstream support, +not to the maturity of the Lua 5.1 extractor, schema, or analysis libraries. diff --git a/lua/ql/src/experimental/Security/CWE-078/CommandInjection.qhelp b/lua/ql/src/experimental/Security/CWE-078/CommandInjection.qhelp new file mode 100644 index 000000000000..66d170774c61 --- /dev/null +++ b/lua/ql/src/experimental/Security/CWE-078/CommandInjection.qhelp @@ -0,0 +1,38 @@ + + + + +

    Building a system command from externally controlled data can allow an attacker to execute +additional commands or change the intended command.

    + +

    This query follows unsanitized data through Lua 5.1 bytecode to command-execution APIs. Lua +source must first be compiled with luac5.1, preserving its relative file layout.

    + +
    + + +

    Avoid constructing shell commands from externally controlled strings. Prefer fixed commands +and map validated input to a small allowlist of arguments or operations.

    + +

    When a command must contain external data, validate or sanitize the value before it reaches the +command-execution API.

    + +
    + + +

    The following example contrasts unsafe command construction with an allowlist-based approach.

    + + + +
    + + +
  • +OWASP: +Command Injection. +
  • + +
    +
    diff --git a/lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql b/lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql new file mode 100644 index 000000000000..a7b2b631d4fe --- /dev/null +++ b/lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql @@ -0,0 +1,44 @@ +/** + * @name Uncontrolled command line + * @description Using externally controlled values in a command line may allow an attacker to execute malicious commands. + * @kind path-problem + * @problem.severity error + * @security-severity 9.8 + * @precision high + * @id lua/command-injection + * @tags security + * experimental + * external/cwe/cwe-078 + * external/cwe/cwe-088 + */ + +import codeql.lua.RulesSanitizerReport + +class LuaReportFile extends @file { + LuaReportFile() { files(this, _) } + + string getPath() { files(this, result) } + + string getURL() { + exists(string prefix | + sourceLocationPrefix(prefix) and + result = "file://" + prefix + "/" + this.getPath() + ":0:0:0:0" + ) + } + + string toString() { result = this.getPath() } +} + +query predicate edges(LuaFlowNode source, LuaFlowNode sink) { + activeReportFlowStep(source, sink, _, _) +} + +from + LuaReportFile file, LuaFlowNode source, LuaFlowNode sink, string classification, string reason, + string provenance +where + activeReportPath(source, sink, classification, reason, provenance) and + file.getPath() = sink.getModulePath() +select file, source, sink, + "Unsanitized Lua bytecode flow from " + source.toString() + " reaches this sink. Classification: " + + classification + "; reason: " + reason + "; provenance: " + provenance + "." diff --git a/lua/ql/src/experimental/Security/CWE-078/examples/CommandInjection.lua b/lua/ql/src/experimental/Security/CWE-078/examples/CommandInjection.lua new file mode 100644 index 000000000000..df01b44d584f --- /dev/null +++ b/lua/ql/src/experimental/Security/CWE-078/examples/CommandInjection.lua @@ -0,0 +1,15 @@ +local requested_action = luci.http.formvalue("action") + +-- BAD: external data becomes part of a shell command. +os.execute("service " .. requested_action .. " restart") + +local commands = { + status = "service app status", + restart = "service app restart" +} + +-- GOOD: external data selects a fixed command. +local command = commands[requested_action] +if command then + os.execute(command) +end diff --git a/lua/ql/src/qlpack.yml b/lua/ql/src/qlpack.yml new file mode 100644 index 000000000000..1d24fec3a9e9 --- /dev/null +++ b/lua/ql/src/qlpack.yml @@ -0,0 +1,12 @@ +name: codeql/lua-queries +version: 0.1.0-dev +groups: + - lua + - queries +suites: codeql-suites +defaultSuiteFile: codeql-suites/lua-code-scanning.qls +dependencies: + codeql/lua-all: ${workspace} + codeql/suite-helpers: ${workspace} +extractor: lua +warnOnImplicitThis: true diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.expected b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.expected new file mode 100644 index 000000000000..9dc2d71f081e --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.expected @@ -0,0 +1,48 @@ +edges +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc0:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc1:r0 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc1:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc2:r0 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc2:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r0 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc3:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc8:r0 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc15:r0 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r0 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc5:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc6:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc6:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc11:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc6:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc12:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc7:r2 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc7:r2 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc11:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc8:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc8:r3 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc8:r3 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r3 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc9:r4 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r4 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r2 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc11:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r3 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r4 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc10:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc12:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc13:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc12:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc14:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc12:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc15:r1 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc15:r1 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc15:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc15:r2 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc16:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc15:r2 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc20:r2 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc18:r3 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc19:r3 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc19:r3 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc21:r3 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc20:r2 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc20:r4 | +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc20:r4 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc21:r4 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc0:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc1:r0 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc1:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc2:r0 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc2:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r0 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc3:r1 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r1 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r0 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r1 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r0 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r2 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r2 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc7:r2 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc7:r1 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r1 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc8:r2 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc9:r2 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc9:r2 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc13:r2 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc10:r3 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r3 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc10:r3 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc13:r3 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r1 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r4 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r4 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r4 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r3 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc13:r3 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r4 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r3 | +#select +| CommandInjection.luac:0:0:0:0 | CommandInjection.luac | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc4:r0 | CommandInjection.luac:0:0:0:0 | CommandInjection.luac::root@pc11:r2 | Unsanitized Lua bytecode flow from CommandInjection.luac::root@pc4:r0 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.lua b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.lua new file mode 100644 index 000000000000..df01b44d584f --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.lua @@ -0,0 +1,15 @@ +local requested_action = luci.http.formvalue("action") + +-- BAD: external data becomes part of a shell command. +os.execute("service " .. requested_action .. " restart") + +local commands = { + status = "service app status", + restart = "service app restart" +} + +-- GOOD: external data selects a fixed command. +local command = commands[requested_action] +if command then + os.execute(command) +end diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.luac b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.luac new file mode 100644 index 000000000000..4f497bed203e Binary files /dev/null and b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.luac differ diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.qlref b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.qlref new file mode 100644 index 000000000000..3789c1f315c6 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.qlref @@ -0,0 +1 @@ +query: experimental/Security/CWE-078/CommandInjection.ql diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/README.md b/lua/ql/test/experimental/query-tests/qhelp-examples/README.md new file mode 100644 index 000000000000..384e1ec4ec47 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/README.md @@ -0,0 +1,16 @@ +# Qhelp example contract + +This test runs the public experimental queries over stripped Lua 5.1 bytecode +compiled from the adjacent source files. The source files are byte-identical to +the examples referenced by the two qhelp files. + +Regenerate the bytecode from this directory with: + +```bash +luac5.1 -s -o CommandInjection.luac CommandInjection.lua +luac5.1 -s -o SanitizedCommandFlow.luac SanitizedCommandFlow.lua +``` + +The active-query oracle contains only the unsafe sink from +`CommandInjection.lua`; the fixed-command branch is intentionally absent. The +sanitized-query oracle contains the path from `SanitizedCommandFlow.lua`. diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.expected b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.expected new file mode 100644 index 000000000000..c454fb465e77 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.expected @@ -0,0 +1,12 @@ +edges +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r0 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r2 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc6:r2 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc7:r2 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc7:r1 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r1 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc7:r2 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc7:r1 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r1 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r4 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc11:r4 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r4 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r3 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc13:r3 | +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r4 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc12:r3 | +#select +| SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc4:r0 | SanitizedCommandFlow.luac:0:0:0:0 | SanitizedCommandFlow.luac::root@pc13:r3 | Sanitized Lua bytecode flow from SanitizedCommandFlow.luac::root@pc4:r0 reaches this sink. Classification: sanitized; reason: sanitized path suppressed. | diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.lua b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.lua new file mode 100644 index 000000000000..1d58d042902f --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.lua @@ -0,0 +1,4 @@ +local host = luci.http.formvalue("host") +local safe_host = shellquote(host) + +os.execute("ping -c 1 " .. safe_host) diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.luac b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.luac new file mode 100644 index 000000000000..416340a9dc63 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.luac differ diff --git a/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.qlref b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.qlref new file mode 100644 index 000000000000..f1c7f4c9123b --- /dev/null +++ b/lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.qlref @@ -0,0 +1 @@ +query: experimental/Diagnostics/SanitizedCommandFlow.ql diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.expected b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.expected new file mode 100644 index 000000000000..5939002d603f --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.expected @@ -0,0 +1,526 @@ +edges +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.0@pc0:r0 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.0@pc1:r0 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.0@pc1:r0 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc3:r2 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.1:r0 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.1@pc0:r0 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc0:r0 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc2:r0 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc1:r1 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc7:r1 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc1:r1 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc11:r1 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc2:r0 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc2:r2 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc2:r2 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc3:r2 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc3:r2 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc5:r2 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc4:r3 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc8:r3 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc4:r3 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc12:r3 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc7:r1 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc7:r4 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc7:r4 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc9:r4 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc8:r3 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc8:r5 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc8:r5 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc9:r5 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc9:r5 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.1:r0 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc11:r1 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc11:r4 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc11:r4 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc13:r4 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc12:r3 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc12:r5 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc12:r5 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc13:r5 | +| bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root@pc13:r5 | bc-branch-negative/input.luac:0:0:0:0 | bc-branch-negative/input.luac::root.1:r0 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root.0@pc0:r0 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root.0@pc1:r0 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root.0@pc1:r0 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc3:r2 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root.1:r0 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root.1@pc0:r0 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc0:r0 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc2:r0 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc1:r1 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc5:r1 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc2:r0 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc2:r2 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc2:r2 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc3:r2 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc4:r2 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc6:r2 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc5:r1 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc5:r3 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc5:r3 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc7:r3 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc6:r2 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc6:r4 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc6:r4 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc7:r4 | +| bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root@pc7:r4 | bc-kill-overwrite/input.luac:0:0:0:0 | bc-kill-overwrite/input.luac::root.1:r0 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root.0@pc0:r0 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root.0@pc1:r0 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root.0@pc1:r0 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc3:r2 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root.1:r0 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root.1@pc0:r0 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc0:r0 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc2:r0 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc1:r1 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc4:r1 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc2:r0 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc2:r2 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc2:r2 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc3:r2 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc3:r2 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc5:r2 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc4:r1 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc4:r3 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc4:r3 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc6:r3 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc5:r2 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc5:r4 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc5:r4 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc6:r4 | +| bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root@pc6:r4 | bc-taint-minimal-path/input.luac:0:0:0:0 | bc-taint-minimal-path/input.luac::root.1:r0 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0:r0 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc0:r0 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc0:r0 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc0:r1 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc0:r1 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc1:r1 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc1:r1 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r2 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0@pc1:r1 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc0:r0 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc3:r0 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc0:r0 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc6:r0 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc1:r1 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc2:r1 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc2:r1 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc4:r1 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc3:r0 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc3:r2 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc3:r2 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r2 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc4:r1 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc4:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc4:r3 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r2 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc12:r2 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r3 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0:r0 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r3 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc5:r2 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc6:r0 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc6:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc6:r3 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc7:r4 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r4 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r3 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc10:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r4 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root.0:r0 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r4 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc8:r3 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc9:r4 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc11:r4 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc10:r3 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc10:r5 | +| callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc10:r5 | callsite-balanced-report/input.luac:0:0:0:0 | callsite-balanced-report/input.luac::root@pc11:r5 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc0:r0 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc1:r0 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc1:r0 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc2:r0 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc2:r0 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc4:r0 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc3:r1 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc4:r1 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc4:r0 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc7:r0 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc4:r1 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc4:r0 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc5:r1 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc6:r1 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc6:r1 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc8:r1 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc7:r0 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc7:r2 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc7:r2 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc8:r2 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc9:r1 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc10:r1 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc10:r1 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc12:r1 | +| constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc11:r2 | constant-sink-overmatch-negative/input.luac:0:0:0:0 | constant-sink-overmatch-negative/input.luac::root@pc12:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root.0:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root.0@pc0:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc0:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc2:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc0:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc3:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc1:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc2:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc2:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc3:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc3:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc4:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc3:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc16:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc4:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc5:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc5:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc7:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc5:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc8:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc6:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc7:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc7:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc8:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc8:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc9:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc9:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc10:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc9:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc24:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc10:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc12:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc11:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc12:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc13:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc15:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc14:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc15:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc15:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc21:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc15:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc15:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc16:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc17:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc17:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc18:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc18:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc19:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc21:r0 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc21:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc21:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r3 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r3 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r3 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc24:r3 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc25:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc25:r3 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc27:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r4 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r4 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc27:r4 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0:r0 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r0 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r0 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r2 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r2 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc2:r2 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc2:r1 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc3:r1 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc3:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc0:r0 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc2:r0 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc0:r0 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc3:r0 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc1:r1 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc2:r1 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc2:r1 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root@pc3:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.0:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.0@pc0:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc0:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc1:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc1:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc2:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc2:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc4:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc3:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc4:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc4:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc7:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc4:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc4:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc5:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc6:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc6:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc8:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc7:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc7:r2 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc7:r2 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc8:r2 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc8:r2 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc0:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc2:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc0:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc3:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc1:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc2:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc2:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc3:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc3:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc4:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc4:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc5:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc5:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc7:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc6:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc7:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc8:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc10:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc9:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc10:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc10:r0 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc12:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc10:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc10:r0 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc11:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc13:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc13:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc14:r1 | +| cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc14:r1 | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root@pc15:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.0:r0 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.0@pc0:r0 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1:r0 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc2:r0 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc0:r1 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc1:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc1:r1 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc3:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc2:r0 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc2:r2 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc2:r2 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc3:r2 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc0:r0 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc6:r0 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc0:r0 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc7:r0 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc1:r1 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc3:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc1:r1 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc4:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc2:r2 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc3:r2 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc3:r2 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc4:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc5:r1 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc6:r1 | +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc6:r1 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root@pc7:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root.0:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root.0@pc0:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root.1:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root.1@pc0:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc0:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc2:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc0:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc3:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc1:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc2:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc2:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc3:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc3:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc4:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc3:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc12:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc4:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc5:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc5:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc7:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc5:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc8:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc6:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc7:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc7:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc8:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc8:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc10:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc8:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc11:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc9:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc10:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc10:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc11:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc11:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc17:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc12:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc13:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc13:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc14:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc14:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc16:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc15:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc16:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc16:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc19:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc16:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc16:r0 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc17:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc18:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc18:r1 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc20:r1 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc19:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc19:r2 | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc19:r2 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc20:r2 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root.0:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root.0@pc0:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root.1:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root.1@pc0:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc0:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc2:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc0:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc3:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc1:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc2:r1 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc2:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc3:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc3:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc4:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc3:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc12:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc4:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc5:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc5:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc7:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc5:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc8:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc6:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc7:r1 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc7:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc8:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc8:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc10:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc8:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc11:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc9:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc10:r1 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc10:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc11:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc11:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc18:r2 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc12:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc13:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc13:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc14:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc14:r0 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc16:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc15:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc16:r1 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc16:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc16:r0 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc17:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc20:r1 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc18:r2 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc19:r2 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc19:r2 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc21:r2 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc20:r1 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc20:r3 | +| no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc20:r3 | no-report-without-path-negative/input.luac:0:0:0:0 | no-report-without-path-negative/input.luac::root@pc21:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root.0:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root.0@pc0:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc0:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc2:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc0:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc3:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc1:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc2:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc2:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc3:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc3:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc4:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc3:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc16:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc4:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc5:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc5:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc7:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc5:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc8:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc6:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc7:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc7:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc8:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc8:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc9:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc9:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc10:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc9:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc24:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc10:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc12:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc11:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc12:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc13:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc15:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc14:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc15:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc15:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc21:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc15:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc15:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc16:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc17:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc17:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc18:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc18:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc19:r2 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r2 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r2 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc21:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc21:r2 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r3 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r2 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r2 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r3 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc24:r3 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc25:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc25:r3 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc27:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r2 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r4 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r4 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc27:r4 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0:r0 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0@pc0:r0 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc0:r0 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc2:r0 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc0:r0 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc3:r0 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc1:r1 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc2:r1 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc2:r1 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root@pc3:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.0:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.0@pc0:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1@pc0:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.2:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.2@pc0:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc0:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc2:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc0:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc3:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc1:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc2:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc2:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc3:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc3:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc4:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc3:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc13:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc4:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc5:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc5:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc7:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc6:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc7:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc6:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc9:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc8:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc18:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc9:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc11:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc9:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc12:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc10:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc11:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc11:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc12:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc12:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc21:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc13:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc14:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc14:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc15:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc15:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc16:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc18:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc18:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc21:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc22:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc22:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc24:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r4 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r4 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc24:r4 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc24:r4 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.0:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.0:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.0@pc0:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.1:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.1@pc0:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.2:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.2@pc0:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc0:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc2:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc0:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc3:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc1:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc2:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc2:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc3:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc3:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc4:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc3:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc13:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc4:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc5:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc5:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc7:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc6:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc7:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc6:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc9:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc8:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc19:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc9:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc11:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc9:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc12:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc10:r2 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc11:r2 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc11:r2 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc12:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc12:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc22:r3 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc13:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc14:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc14:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc15:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc15:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc16:r2 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r2 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc24:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r2 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r1 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc18:r2 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc20:r2 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc19:r0 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc19:r3 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc19:r3 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc21:r3 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc20:r2 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc20:r4 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc20:r4 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc21:r4 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc21:r4 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.1:r0 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc22:r3 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc23:r3 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc23:r3 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc25:r3 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc24:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc24:r4 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc24:r4 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc25:r4 | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc25:r4 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root.0:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root.0:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root.0@pc0:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root.1:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root.1@pc0:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc0:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc2:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc0:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc3:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc1:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc2:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc2:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc3:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc3:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc4:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc3:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc14:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc4:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc5:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc5:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc7:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc6:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc7:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc6:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc9:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc8:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc23:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc9:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc10:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc10:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc11:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc10:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc29:r3 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc11:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc13:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc12:r2 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc13:r2 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc14:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc15:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc15:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc16:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc16:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc17:r2 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r2 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc24:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc28:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r2 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r1 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc20:r3 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc21:r3 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc23:r0 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc23:r3 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc24:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc24:r4 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc24:r4 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc25:r4 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc25:r3 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc26:r3 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc25:r4 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root.1:r0 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc26:r2 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc31:r2 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc26:r3 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc26:r2 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc28:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc28:r2 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc28:r2 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc31:r2 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc29:r3 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc30:r3 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc30:r3 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc32:r3 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc31:r2 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc31:r4 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc31:r4 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc32:r4 | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc32:r4 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root.0:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.0:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.0@pc0:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.1:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.1@pc0:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc0:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc1:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc1:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc2:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc2:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc4:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc3:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc4:r1 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc4:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc7:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc4:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc4:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc5:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc6:r1 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc6:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc8:r1 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc7:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc7:r2 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc7:r2 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc8:r2 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc0:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc2:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc0:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc3:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc1:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc2:r1 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc2:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc3:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc3:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc4:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc4:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc5:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc5:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc7:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc5:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc8:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc6:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc7:r1 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc7:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc8:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc8:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc10:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc8:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc11:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc9:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc10:r1 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc10:r1 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc11:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc12:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc13:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc13:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc14:r0 | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc14:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root@pc15:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc0:r0 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc0:r0 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc0:r0 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc13:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc0:r0 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc1:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc2:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc2:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc11:r3 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc13:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc3:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r2 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc4:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc5:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc5:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc6:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc8:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc9:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc9:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc13:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc11:r0 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc11:r3 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc11:r3 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc12:r3 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc12:r2 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc13:r2 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc13:r2 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r0 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc14:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc16:r1 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r0 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r2 | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc15:r2 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc16:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc0:r0 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc0:r0 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc0:r0 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc0:r0 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc1:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc2:r1 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc2:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r1 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc3:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc4:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc5:r1 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc5:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc6:r1 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc9:r0 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc9:r2 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc10:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc10:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc11:r1 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc11:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r0 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc11:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc12:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc14:r1 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r0 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r2 | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc13:r2 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc1:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc2:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc2:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc4:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc5:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc5:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc6:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc10:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc10:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc15:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc15:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc17:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc19:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc19:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc16:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc0:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc1:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc2:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc2:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc11:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc16:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc4:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc5:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc5:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc6:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc9:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc9:r2 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc10:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc10:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc11:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc16:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc14:r2 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc15:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc15:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc16:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc16:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc17:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc19:r1 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r0 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc18:r2 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc19:r2 | +#select +| cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac | cross-module-webcmd-popen/controller.luac:0:0:0:0 | cross-module-webcmd-popen/controller.luac::root.1@pc4:r0 | cross-module-webcmd-popen/mtkwifi.luac:0:0:0:0 | cross-module-webcmd-popen/mtkwifi.luac::root.1@pc3:r2 | Unsanitized Lua bytecode flow from cross-module-webcmd-popen/controller.luac::root.1@pc4:r0 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc16:r0 | formvalue-os-execute-chain/input.luac:0:0:0:0 | formvalue-os-execute-chain/input.luac::root@pc20:r2 | Unsanitized Lua bytecode flow from formvalue-os-execute-chain/input.luac::root@pc16:r0 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r1 | sanitizer-same-suffix-off-chain-negative/input.luac:0:0:0:0 | sanitizer-same-suffix-off-chain-negative/input.luac::root@pc25:r4 | Unsanitized Lua bytecode flow from sanitizer-same-suffix-off-chain-negative/input.luac::root@pc17:r1 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc18:r1 | sanitizer-unsanitized-alternative/input.luac:0:0:0:0 | sanitizer-unsanitized-alternative/input.luac::root@pc32:r4 | Unsanitized Lua bytecode flow from sanitizer-unsanitized-alternative/input.luac::root@pc18:r1 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc4:r0 | submit-dpp-uri-execute/input.luac:0:0:0:0 | submit-dpp-uri-execute/input.luac::root.2@pc8:r2 | Unsanitized Lua bytecode flow from submit-dpp-uri-execute/input.luac::root.2@pc4:r0 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc2:r1 | table-field-sanitizer-overwrite/dynamic-key.luac:0:0:0:0 | table-field-sanitizer-overwrite/dynamic-key.luac::root@pc16:r2 | Unsanitized Lua bytecode flow from table-field-sanitizer-overwrite/dynamic-key.luac::root@pc2:r1 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc2:r1 | table-field-sanitizer-overwrite/optional-branch.luac:0:0:0:0 | table-field-sanitizer-overwrite/optional-branch.luac::root@pc14:r2 | Unsanitized Lua bytecode flow from table-field-sanitizer-overwrite/optional-branch.luac::root@pc2:r1 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | +| table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc2:r1 | table-field-sanitizer-overwrite/unrelated-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/unrelated-field.luac::root@pc19:r2 | Unsanitized Lua bytecode flow from table-field-sanitizer-overwrite/unrelated-field.luac::root@pc2:r1 reaches this sink. Classification: true-positive; reason: unsanitized active source-to-sink path; provenance: bytecode-only,ql-native-active-report-path. | diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.qlref b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.qlref new file mode 100644 index 000000000000..3789c1f315c6 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.qlref @@ -0,0 +1 @@ +query: experimental/Security/CWE-078/CommandInjection.ql diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.expected b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.expected new file mode 100644 index 000000000000..724a52afa717 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.expected @@ -0,0 +1,61 @@ +edges +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r1 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc22:r3 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r3 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r3 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r3 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0:r0 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r2 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r4 | +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc26:r4 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc27:r4 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0:r0 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r0 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r0 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r2 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc1:r2 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc2:r2 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc2:r1 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc3:r1 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc2:r2 | cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc2:r1 | +| cross-module-sanitizer/sanitizer.luac:0:0:0:0 | cross-module-sanitizer/sanitizer.luac::root.0@pc3:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc23:r2 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r1 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc22:r3 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r3 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r2 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r2 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r3 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r2 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r3 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0:r0 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r2 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r4 | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc26:r4 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc27:r4 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0:r0 | sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0@pc0:r0 | +| sanitizer-cross-module-return/shellsafe.luac:0:0:0:0 | sanitizer-cross-module-return/shellsafe.luac::root.0@pc0:r0 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc23:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1@pc0:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1@pc0:r0 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r1 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc19:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r3 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root.1:r0 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r3 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc20:r2 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r2 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r4 | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc23:r4 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc24:r4 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc2:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc3:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc9:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc10:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc10:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc10:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc10:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc11:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc14:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc15:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc15:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc15:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc15:r1 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc16:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r0 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc18:r2 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc19:r2 | +#select +| cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc20:r1 | cross-module-sanitizer/controller.luac:0:0:0:0 | cross-module-sanitizer/controller.luac::root@pc27:r4 | Sanitized Lua bytecode flow from cross-module-sanitizer/controller.luac::root@pc20:r1 reaches this sink. Classification: sanitized; reason: sanitized path suppressed. | +| sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc20:r1 | sanitizer-cross-module-return/controller.luac:0:0:0:0 | sanitizer-cross-module-return/controller.luac::root@pc27:r4 | Sanitized Lua bytecode flow from sanitizer-cross-module-return/controller.luac::root@pc20:r1 reaches this sink. Classification: sanitized; reason: sanitized path suppressed. | +| sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc17:r1 | sanitizer-on-path/input.luac:0:0:0:0 | sanitizer-on-path/input.luac::root@pc24:r4 | Sanitized Lua bytecode flow from sanitizer-on-path/input.luac::root@pc17:r1 reaches this sink. Classification: sanitized; reason: sanitized path suppressed. | +| table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc2:r1 | table-field-sanitizer-overwrite/same-field.luac:0:0:0:0 | table-field-sanitizer-overwrite/same-field.luac::root@pc19:r2 | Sanitized Lua bytecode flow from table-field-sanitizer-overwrite/same-field.luac::root@pc2:r1 reaches this sink. Classification: sanitized; reason: sanitized path suppressed. | diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.qlref b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.qlref new file mode 100644 index 000000000000..f1c7f4c9123b --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.qlref @@ -0,0 +1 @@ +query: experimental/Diagnostics/SanitizedCommandFlow.ql diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.expected b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.expected new file mode 100644 index 000000000000..da208cd64806 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.expected @@ -0,0 +1,96 @@ +| bc-branch-negative/input.luac | source-endpoint | root@pc3 *.source -> root@pc3:r2 bytecode-only,ql-source-rule,typed-call-resolution | +| bc-branch-negative/input.luac | source-sink.rule-match | root@pc3 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| bc-kill-overwrite/input.luac | source-endpoint | root@pc3 *.source -> root@pc3:r2 bytecode-only,ql-source-rule,typed-call-resolution | +| bc-kill-overwrite/input.luac | source-sink.rule-match | root@pc3 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| bc-taint-minimal-path/input.luac | source-endpoint | root@pc3 *.source -> root@pc3:r2 bytecode-only,ql-source-rule,typed-call-resolution | +| bc-taint-minimal-path/input.luac | source-sink.rule-match | root@pc3 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| callsite-balanced-report/input.luac | sink-endpoint | root@pc11 *.execute param=0 -> root@pc11:r5 bytecode-only,ql-sink-rule,typed-call-resolution | +| callsite-balanced-report/input.luac | source-endpoint | root@pc2 *.source -> root@pc2:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| callsite-balanced-report/input.luac | source-sink.rule-match | root@pc2 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| callsite-balanced-report/input.luac | source-sink.rule-match | root@pc11 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| cross-module-sanitizer/controller.luac | report.classification | root@pc20:r1 -> root@pc27:r4 sanitized sanitized path suppressed | +| cross-module-sanitizer/controller.luac | report.sanitized-positive-only | cross-module-sanitizer/controller.luac::root@pc20:r1 -> cross-module-sanitizer/controller.luac::root@pc27:r4 | +| cross-module-sanitizer/controller.luac | sanitizer.classification | root@pc20:r1 -> root@pc27:r4 sanitizer=root.0@pc2:shellquote applies=true chain=true classification=sanitized | +| cross-module-sanitizer/controller.luac | sanitizer.classification.callee-module | root@pc20:r1 -> root@pc27:r4 sanitizer=cross-module-sanitizer/sanitizer.luac::root.0@pc2:shellquote classification=sanitized | +| cross-module-sanitizer/controller.luac | sink-endpoint | root@pc27 *.execute param=0 -> root@pc27:r4 bytecode-only,ql-sink-rule,typed-call-resolution | +| cross-module-sanitizer/controller.luac | source-endpoint | root@pc20 *.formvalue -> root@pc20:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| cross-module-sanitizer/controller.luac | source-sink.rule-match | root@pc20 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| cross-module-sanitizer/controller.luac | source-sink.rule-match | root@pc27 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| cross-module-sanitizer/sanitizer.luac | sanitizer-call.callee-module | root.0@pc2 shellquote -> root.0@pc2:r1 typed-call-resolution | +| cross-module-webcmd-popen/controller.luac | report.classification | root.1@pc4:r0 -> root.1@pc3:r2 true-positive unsanitized active source-to-sink path | +| cross-module-webcmd-popen/controller.luac | source-endpoint | root.1@pc4 *.formvalue -> root.1@pc4:r0 bytecode-only,ql-source-rule,typed-call-resolution | +| cross-module-webcmd-popen/controller.luac | source-sink.rule-match | root.1@pc4 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| cross-module-webcmd-popen/mtkwifi.luac | sink-endpoint | root.1@pc3 *.popen param=0 -> root.1@pc3:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| cross-module-webcmd-popen/mtkwifi.luac | source-sink.rule-match | root.1@pc3 sink *.popen -> popen param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| formvalue-os-execute-chain/input.luac | report.classification | root@pc16:r0 -> root@pc20:r2 true-positive unsanitized active source-to-sink path | +| formvalue-os-execute-chain/input.luac | report.path.local-complete | formvalue-os-execute-chain/input.luac::root@pc16:r0 -> formvalue-os-execute-chain/input.luac::root@pc19:r0 -> formvalue-os-execute-chain/input.luac::root@pc19:r2 -> formvalue-os-execute-chain/input.luac::root@pc20:r2 | +| formvalue-os-execute-chain/input.luac | sink-endpoint | root@pc20 *.execute param=0 -> root@pc20:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| formvalue-os-execute-chain/input.luac | source-endpoint | root@pc16 *.formvalue -> root@pc16:r0 bytecode-only,ql-source-rule,typed-call-resolution | +| formvalue-os-execute-chain/input.luac | source-endpoint.typed-resolution | root@pc16 *.formvalue -> root@pc16:r0 typed-call-resolution | +| formvalue-os-execute-chain/input.luac | source-sink.rule-match | root@pc16 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| formvalue-os-execute-chain/input.luac | source-sink.rule-match | root@pc20 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| no-report-without-path-negative/input.luac | sink-endpoint | root@pc21 *.execute param=0 -> root@pc21:r3 bytecode-only,ql-sink-rule,typed-call-resolution | +| no-report-without-path-negative/input.luac | source-endpoint | root@pc16 *.formvalue -> root@pc16:r0 bytecode-only,ql-source-rule,typed-call-resolution | +| no-report-without-path-negative/input.luac | source-sink.rule-match | root@pc16 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| no-report-without-path-negative/input.luac | source-sink.rule-match | root@pc21 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-cross-module-return/controller.luac | report.classification | root@pc20:r1 -> root@pc27:r4 sanitized sanitized path suppressed | +| sanitizer-cross-module-return/controller.luac | report.sanitized-positive-only | sanitizer-cross-module-return/controller.luac::root@pc20:r1 -> sanitizer-cross-module-return/controller.luac::root@pc27:r4 | +| sanitizer-cross-module-return/controller.luac | sanitizer-call.cross-module | root@pc23 shellquote -> root@pc23:r2 typed-call-resolution | +| sanitizer-cross-module-return/controller.luac | sanitizer.classification | root@pc20:r1 -> root@pc27:r4 sanitizer=root@pc23:shellquote applies=true chain=true classification=sanitized | +| sanitizer-cross-module-return/controller.luac | sanitizer.classification.cross-module | root@pc20:r1 -> root@pc27:r4 sanitizer=root@pc23:shellquote classification=sanitized | +| sanitizer-cross-module-return/controller.luac | sink-endpoint | root@pc27 *.execute param=0 -> root@pc27:r4 bytecode-only,ql-sink-rule,typed-call-resolution | +| sanitizer-cross-module-return/controller.luac | source-endpoint | root@pc20 *.formvalue -> root@pc20:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| sanitizer-cross-module-return/controller.luac | source-sink.rule-match | root@pc20 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-cross-module-return/controller.luac | source-sink.rule-match | root@pc27 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-on-path/input.luac | report.classification | root@pc17:r1 -> root@pc24:r4 sanitized sanitized path suppressed | +| sanitizer-on-path/input.luac | report.sanitized-positive-only | sanitizer-on-path/input.luac::root@pc17:r1 -> sanitizer-on-path/input.luac::root@pc24:r4 | +| sanitizer-on-path/input.luac | sanitizer-call.typed-resolution | root@pc20 tonumber -> root@pc20:r2 typed-call-resolution | +| sanitizer-on-path/input.luac | sanitizer.classification | root@pc17:r1 -> root@pc24:r4 sanitizer=root@pc20:tonumber applies=true chain=true classification=sanitized | +| sanitizer-on-path/input.luac | sink-endpoint | root@pc24 *.execute param=0 -> root@pc24:r4 bytecode-only,ql-sink-rule,typed-call-resolution | +| sanitizer-on-path/input.luac | source-endpoint | root@pc17 *.formvalue -> root@pc17:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| sanitizer-on-path/input.luac | source-sink.rule-match | root@pc17 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-on-path/input.luac | source-sink.rule-match | root@pc24 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-same-suffix-off-chain-negative/input.luac | report.classification | root@pc17:r1 -> root@pc25:r4 true-positive unsanitized active source-to-sink path | +| sanitizer-same-suffix-off-chain-negative/input.luac | sanitizer.classification | root@pc17:r1 -> root@pc25:r4 sanitizer=root@pc21:tonumber applies=false chain=false classification=not-sanitized | +| sanitizer-same-suffix-off-chain-negative/input.luac | sink-endpoint | root@pc25 *.execute param=0 -> root@pc25:r4 bytecode-only,ql-sink-rule,typed-call-resolution | +| sanitizer-same-suffix-off-chain-negative/input.luac | source-endpoint | root@pc17 *.formvalue -> root@pc17:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| sanitizer-same-suffix-off-chain-negative/input.luac | source-sink.rule-match | root@pc17 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-same-suffix-off-chain-negative/input.luac | source-sink.rule-match | root@pc25 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-unsanitized-alternative/input.luac | report.classification | root@pc18:r1 -> root@pc32:r4 true-positive unsanitized active source-to-sink path | +| sanitizer-unsanitized-alternative/input.luac | sanitizer.classification | root@pc18:r1 -> root@pc32:r4 sanitizer=root@pc25:shellquote applies=true chain=true classification=not-sanitized | +| sanitizer-unsanitized-alternative/input.luac | sink-endpoint | root@pc32 *.execute param=0 -> root@pc32:r4 bytecode-only,ql-sink-rule,typed-call-resolution | +| sanitizer-unsanitized-alternative/input.luac | source-endpoint | root@pc18 *.formvalue -> root@pc18:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| sanitizer-unsanitized-alternative/input.luac | source-sink.rule-match | root@pc18 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| sanitizer-unsanitized-alternative/input.luac | source-sink.rule-match | root@pc32 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| submit-dpp-uri-execute/input.luac | report.classification | root.2@pc4:r0 -> root.2@pc8:r2 true-positive unsanitized active source-to-sink path | +| submit-dpp-uri-execute/input.luac | sink-endpoint | root.2@pc8 *.execute param=0 -> root.2@pc8:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| submit-dpp-uri-execute/input.luac | source-endpoint | root.2@pc4 *.formvalue -> root.2@pc4:r0 bytecode-only,ql-source-rule,typed-call-resolution | +| submit-dpp-uri-execute/input.luac | source-sink.rule-match | root.2@pc4 source *.formvalue -> formvalue param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| submit-dpp-uri-execute/input.luac | source-sink.rule-match | root.2@pc8 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/dynamic-key.luac | report.classification | root@pc2:r1 -> root@pc16:r2 true-positive unsanitized active source-to-sink path | +| table-field-sanitizer-overwrite/dynamic-key.luac | sanitizer.classification | root@pc2:r1 -> root@pc16:r2 sanitizer=root@pc12:tonumber applies=true chain=true classification=not-sanitized | +| table-field-sanitizer-overwrite/dynamic-key.luac | sink-endpoint | root@pc16 *.execute param=0 -> root@pc16:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/dynamic-key.luac | source-endpoint | root@pc2 *.source -> root@pc2:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/dynamic-key.luac | source-sink.rule-match | root@pc2 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/dynamic-key.luac | source-sink.rule-match | root@pc16 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/optional-branch.luac | report.classification | root@pc2:r1 -> root@pc14:r2 true-positive unsanitized active source-to-sink path | +| table-field-sanitizer-overwrite/optional-branch.luac | sanitizer.classification | root@pc2:r1 -> root@pc14:r2 sanitizer=root@pc10:tonumber applies=true chain=true classification=not-sanitized | +| table-field-sanitizer-overwrite/optional-branch.luac | sink-endpoint | root@pc14 *.execute param=0 -> root@pc14:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/optional-branch.luac | source-endpoint | root@pc2 *.source -> root@pc2:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/optional-branch.luac | source-sink.rule-match | root@pc2 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/optional-branch.luac | source-sink.rule-match | root@pc14 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/same-field.luac | report.classification | root@pc2:r1 -> root@pc19:r2 sanitized sanitized path suppressed | +| table-field-sanitizer-overwrite/same-field.luac | report.sanitized-positive-only | table-field-sanitizer-overwrite/same-field.luac::root@pc2:r1 -> table-field-sanitizer-overwrite/same-field.luac::root@pc19:r2 | +| table-field-sanitizer-overwrite/same-field.luac | sanitizer.classification | root@pc2:r1 -> root@pc19:r2 sanitizer=root@pc10:tonumber applies=true chain=true classification=sanitized | +| table-field-sanitizer-overwrite/same-field.luac | sanitizer.classification | root@pc2:r1 -> root@pc19:r2 sanitizer=root@pc15:shellquote applies=true chain=true classification=sanitized | +| table-field-sanitizer-overwrite/same-field.luac | sink-endpoint | root@pc19 *.execute param=0 -> root@pc19:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/same-field.luac | source-endpoint | root@pc2 *.source -> root@pc2:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/same-field.luac | source-sink.rule-match | root@pc2 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/same-field.luac | source-sink.rule-match | root@pc19 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/unrelated-field.luac | report.classification | root@pc2:r1 -> root@pc19:r2 true-positive unsanitized active source-to-sink path | +| table-field-sanitizer-overwrite/unrelated-field.luac | sanitizer.classification | root@pc2:r1 -> root@pc19:r2 sanitizer=root@pc10:tonumber applies=true chain=true classification=not-sanitized | +| table-field-sanitizer-overwrite/unrelated-field.luac | sanitizer.classification | root@pc2:r1 -> root@pc19:r2 sanitizer=root@pc15:shellquote applies=true chain=true classification=not-sanitized | +| table-field-sanitizer-overwrite/unrelated-field.luac | sink-endpoint | root@pc19 *.execute param=0 -> root@pc19:r2 bytecode-only,ql-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/unrelated-field.luac | source-endpoint | root@pc2 *.source -> root@pc2:r1 bytecode-only,ql-source-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/unrelated-field.luac | source-sink.rule-match | root@pc2 source *.source -> source param=-1 bytecode-only,ql-source-sink-rule,typed-call-resolution | +| table-field-sanitizer-overwrite/unrelated-field.luac | source-sink.rule-match | root@pc19 sink *.execute -> execute param=0 bytecode-only,ql-source-sink-rule,typed-call-resolution | diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.ql b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.ql new file mode 100644 index 000000000000..6c0eeb8e18e6 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.ql @@ -0,0 +1,115 @@ +import codeql.lua.RulesSanitizerReport + +from string fixture, string capability, string evidence +where + capability = "report.path.local-complete" and + fixture = "formvalue-os-execute-chain/input.luac" and + exists(LuaFlowNode source, LuaFlowNode firstMiddle, LuaFlowNode secondMiddle, LuaFlowNode sink | + source.getModulePath() = fixture and + source.getValueRef() = "root@pc16:r0" and + firstMiddle.getModulePath() = fixture and + firstMiddle.getValueRef() = "root@pc19:r0" and + secondMiddle.getModulePath() = fixture and + secondMiddle.getValueRef() = "root@pc19:r2" and + sink.getModulePath() = fixture and + sink.getValueRef() = "root@pc20:r2" and + activeReportPath(source, sink, "true-positive", "unsanitized active source-to-sink path", + "bytecode-only,ql-native-active-report-path") and + activeReportFlowStep(source, firstMiddle, _, _) and + activeReportFlowStep(firstMiddle, secondMiddle, _, _) and + activeReportFlowStep(secondMiddle, sink, _, _) and + evidence = + source.toString() + " -> " + firstMiddle.toString() + " -> " + secondMiddle.toString() + + " -> " + sink.toString() + ) + or + capability = "sanitizer-call.callee-module" and + fixture = "cross-module-sanitizer/sanitizer.luac" and + sanitizerCall(fixture, "root.0@pc2", "shellquote", "root.0@pc2:r1", + "bytecode-only,ql-sanitizer-call,typed-call-resolution") and + evidence = "root.0@pc2 shellquote -> root.0@pc2:r1 typed-call-resolution" + or + capability = "sanitizer.classification.callee-module" and + fixture = "cross-module-sanitizer/controller.luac" and + sanitizerClassification(fixture, "root@pc20:r1", fixture, "root@pc27:r4", + "cross-module-sanitizer/sanitizer.luac", "root.0@pc2", "shellquote", "true", "true", "sanitized") and + evidence = + "root@pc20:r1 -> root@pc27:r4 sanitizer=cross-module-sanitizer/sanitizer.luac::root.0@pc2:shellquote classification=sanitized" + or + capability = "sanitizer-call.typed-resolution" and + fixture = "sanitizer-on-path/input.luac" and + sanitizerCall(fixture, "root@pc20", "tonumber", "root@pc20:r2", + "bytecode-only,ql-sanitizer-call,typed-call-resolution") and + evidence = "root@pc20 tonumber -> root@pc20:r2 typed-call-resolution" + or + capability = "sanitizer-call.cross-module" and + fixture = "sanitizer-cross-module-return/controller.luac" and + sanitizerCall(fixture, "root@pc23", "shellquote", "root@pc23:r2", + "bytecode-only,ql-sanitizer-call,typed-call-resolution") and + evidence = "root@pc23 shellquote -> root@pc23:r2 typed-call-resolution" + or + capability = "sanitizer.classification.cross-module" and + fixture = "sanitizer-cross-module-return/controller.luac" and + sanitizerClassification(fixture, "root@pc20:r1", fixture, "root@pc27:r4", fixture, "root@pc23", + "shellquote", "true", "true", "sanitized") and + evidence = "root@pc20:r1 -> root@pc27:r4 sanitizer=root@pc23:shellquote classification=sanitized" + or + capability = "source-endpoint.typed-resolution" and + fixture = "formvalue-os-execute-chain/input.luac" and + sourceEndpoint(fixture, "root@pc16:r0", "root@pc16", "*.formvalue", + "bytecode-only,ql-source-rule,typed-call-resolution") and + evidence = "root@pc16 *.formvalue -> root@pc16:r0 typed-call-resolution" + or + capability = "source-sink.rule-match" and + exists( + string callsiteId, string ruleKind, string trigger, string matchedName, int parameterIndex, + string provenance + | + sourceSinkRuleMatch(fixture, callsiteId, ruleKind, trigger, matchedName, parameterIndex, + provenance) and + evidence = + callsiteId + " " + ruleKind + " " + trigger + " -> " + matchedName + " param=" + + parameterIndex.toString() + " " + provenance + ) + or + capability = "source-endpoint" and + exists(string sourceRef, string callsiteId, string trigger, string provenance | + sourceEndpoint(fixture, sourceRef, callsiteId, trigger, provenance) and + evidence = callsiteId + " " + trigger + " -> " + sourceRef + " " + provenance + ) + or + capability = "sink-endpoint" and + exists(string sinkRef, string callsiteId, string trigger, int parameterIndex, string provenance | + sinkEndpoint(fixture, sinkRef, callsiteId, trigger, parameterIndex, provenance) and + evidence = + callsiteId + " " + trigger + " param=" + parameterIndex.toString() + " -> " + sinkRef + " " + + provenance + ) + or + capability = "sanitizer.classification" and + exists( + string sourceRef, string sinkModule, string sinkRef, string sanitizerModule, + string sanitizerCallsiteId, string sanitizerName, string appliesToSink, string onDataflowChain, + string classification + | + sanitizerClassification(fixture, sourceRef, sinkModule, sinkRef, sanitizerModule, + sanitizerCallsiteId, sanitizerName, appliesToSink, onDataflowChain, classification) and + evidence = + sourceRef + " -> " + sinkRef + " sanitizer=" + sanitizerCallsiteId + ":" + sanitizerName + + " applies=" + appliesToSink + " chain=" + onDataflowChain + " classification=" + + classification + ) + or + capability = "report.sanitized-positive-only" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + sanitizedReportPath(source, sink) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + capability = "report.classification" and + exists(string sourceRef, string sinkModule, string sinkRef, string classification, string reason | + reportClassification(fixture, sourceRef, sinkModule, sinkRef, classification, reason) and + evidence = sourceRef + " -> " + sinkRef + " " + classification + " " + reason + ) +select fixture, capability, evidence diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.expected b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.expected @@ -0,0 +1 @@ + diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.ql b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.ql new file mode 100644 index 000000000000..617a242bd9fb --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.ql @@ -0,0 +1,116 @@ +import codeql.lua.RulesSanitizerReport + +from string fixture, string forbidden, string evidence +where + fixture = "cross-module-sanitizer/controller.luac" and + forbidden = "callee-sanitized-path-emitted-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + source.getValueRef() = "root@pc20:r1" and + sink.getModulePath() = fixture and + sink.getValueRef() = "root@pc27:r4" and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + fixture = "callsite-balanced-report/input.luac" and + forbidden = "cross-callsite-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + source.getValueRef() = "root@pc2:r1" and + sink.getModulePath() = fixture and + sink.getValueRef() = "root@pc11:r5" and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + fixture = "sanitizer-unsanitized-alternative/input.luac" and + forbidden = "unsanitized-alternative-was-suppressed" and + exists( + string sourceRef, string sinkRef, string sanitizerCallsiteId, string sanitizerName, + string appliesToSink, string onDataflowChain + | + sanitizerClassification(fixture, sourceRef, fixture, sinkRef, fixture, sanitizerCallsiteId, + sanitizerName, appliesToSink, onDataflowChain, "sanitized") and + evidence = sourceRef + " -> " + sinkRef + ) + or + fixture = "constant-sink-overmatch-negative/input.luac" and + forbidden = "formvaluex-overmatched-source-rule" and + exists( + string callsiteId, string ruleKind, string trigger, string matchedName, int parameterIndex, + string provenance + | + sourceSinkRuleMatch(fixture, callsiteId, ruleKind, trigger, matchedName, parameterIndex, + provenance) and + matchedName = "formvaluex" and + evidence = callsiteId + ) + or + fixture = "constant-sink-overmatch-negative/input.luac" and + forbidden = "executex-overmatched-sink-rule" and + exists( + string callsiteId, string ruleKind, string trigger, string matchedName, int parameterIndex, + string provenance + | + sourceSinkRuleMatch(fixture, callsiteId, ruleKind, trigger, matchedName, parameterIndex, + provenance) and + matchedName = "executex" and + evidence = callsiteId + ) + or + fixture = "constant-sink-overmatch-negative/input.luac" and + forbidden = "constant-sink-argument-became-sink-endpoint" and + exists(string sinkRef, string callsiteId, string trigger, int parameterIndex, string provenance | + sinkEndpoint(fixture, sinkRef, callsiteId, trigger, parameterIndex, provenance) and + evidence = callsiteId + " " + sinkRef + ) + or + fixture = "constant-sink-overmatch-negative/input.luac" and + forbidden = "constant-sink-produced-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + fixture = "sanitizer-same-suffix-off-chain-negative/input.luac" and + forbidden = "off-chain-sanitizer-suppressed-report" and + exists(string sourceRef, string sinkRef, string classification, string reason | + reportClassification(fixture, sourceRef, fixture, sinkRef, classification, reason) and + classification = "sanitized" and + evidence = sourceRef + " -> " + sinkRef + ) + or + fixture = "sanitizer-on-path/input.luac" and + forbidden = "sanitized-path-emitted-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + fixture = "no-report-without-path-negative/input.luac" and + forbidden = "endpoint-only-produced-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + fixture = "bc-kill-overwrite/input.luac" and + forbidden = "killed-flow-produced-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) + or + fixture = "bc-branch-negative/input.luac" and + forbidden = "branch-negative-produced-active-report" and + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + activeReportPath(source, sink, _, _, _) and + evidence = source.toString() + " -> " + sink.toString() + ) +select fixture, forbidden, evidence diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/SAMPLE-MANIFEST.md b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/SAMPLE-MANIFEST.md new file mode 100644 index 000000000000..de07a2f68791 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/SAMPLE-MANIFEST.md @@ -0,0 +1,31 @@ +# Rules, sanitizer, and report sample manifest + +These committed Lua 5.1 bytecode fixtures exercise the command-flow finding +pipeline without external test data. + +| Fixture | Expected boundary | +| --- | --- | +| `bc-taint-minimal-path/input.luac` | Minimal proved path can be consumed by report construction. | +| `formvalue-os-execute-chain/input.luac` | Local `*.formvalue` source to `*.execute` sink finding. | +| `submit-dpp-uri-execute/input.luac` | Nested-prototype source-to-execute finding. | +| `cross-module-webcmd-popen/{controller,mtkwifi}.luac` | Cross-module source propagation to a `*.popen` sink. | +| `sanitizer-on-path/input.luac` | An on-path sanitizer suppresses the finding. | +| `table-field-sanitizer-overwrite/*.luac` | A sanitizer result replacing the same static table field suppresses only that mandatory field path; unrelated fields, optional branches, and dynamic keys remain active. | +| `constant-sink-overmatch-negative/input.luac` | Similar names and fixed-string sink arguments do not overmatch. | +| `sanitizer-same-suffix-off-chain-negative/input.luac` | An off-path sanitizer does not suppress the real path. | +| `no-report-without-path-negative/input.luac` | Source and sink endpoints alone do not synthesize a finding. | +| `bc-kill-overwrite/input.luac` | Killed flow does not produce a finding. | +| `bc-branch-negative/input.luac` | No unproved branch flow is synthesized. | + +Reviewer command: + +```bash +CODEQL=/absolute/path/to/codeql +"$CODEQL" test run lua/ql/test/library-tests/rules-sanitizer-report \ + --search-path .:lua --threads=0 --verbosity=progress +``` + +The `.expected` files in this directory are the complete oracle for this +focused test set. For a standalone database/query reproduction using the same +fixtures, follow `lua/README.md` and keep this directory as the common source +root so fixture-relative identities remain stable. diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.expected b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.expected new file mode 100644 index 000000000000..dcee42c24922 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.expected @@ -0,0 +1,4 @@ +| table-field-sanitizer-overwrite/dynamic-key.luac | dynamic-key replacement remains active | +| table-field-sanitizer-overwrite/optional-branch.luac | optional same-field replacement remains active | +| table-field-sanitizer-overwrite/same-field.luac | same static field replacement is sanitized only | +| table-field-sanitizer-overwrite/unrelated-field.luac | unrelated static field replacement remains active | diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.ql b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.ql new file mode 100644 index 000000000000..743a95d196a9 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.ql @@ -0,0 +1,37 @@ +import codeql.lua.RulesSanitizerReport + +private predicate hasActiveReport(string fixture) { + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + sink.getModulePath() = fixture and + activeReportPath(source, sink, _, _, _) + ) +} + +private predicate hasSanitizedReport(string fixture) { + exists(LuaFlowNode source, LuaFlowNode sink | + source.getModulePath() = fixture and + sink.getModulePath() = fixture and + sanitizedReportPath(source, sink) + ) +} + +from string fixture, string behavior +where + fixture = "table-field-sanitizer-overwrite/same-field.luac" and + hasSanitizedReport(fixture) and + not hasActiveReport(fixture) and + behavior = "same static field replacement is sanitized only" + or + fixture = "table-field-sanitizer-overwrite/unrelated-field.luac" and + hasActiveReport(fixture) and + behavior = "unrelated static field replacement remains active" + or + fixture = "table-field-sanitizer-overwrite/optional-branch.luac" and + hasActiveReport(fixture) and + behavior = "optional same-field replacement remains active" + or + fixture = "table-field-sanitizer-overwrite/dynamic-key.luac" and + hasActiveReport(fixture) and + behavior = "dynamic-key replacement remains active" +select fixture, behavior diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-branch-negative/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-branch-negative/input.luac new file mode 100644 index 000000000000..29c4dfe7ad77 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-branch-negative/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-kill-overwrite/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-kill-overwrite/input.luac new file mode 100644 index 000000000000..7ecd2a1f96b9 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-kill-overwrite/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-taint-minimal-path/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-taint-minimal-path/input.luac new file mode 100644 index 000000000000..71021c22082e Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-taint-minimal-path/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/input.luac new file mode 100644 index 000000000000..7553128e3e31 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/source.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/source.lua new file mode 100644 index 000000000000..8b2e4716379f --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/source.lua @@ -0,0 +1,12 @@ +local function identity(value) + local result = value + return result +end + +local tainted = source() +local tainted_result = identity(tainted) +local clean_result = identity("clean") + +execute(clean_result) + +return tainted_result diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/input.luac new file mode 100644 index 000000000000..5395e574a43f Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/source.lua.txt new file mode 100644 index 000000000000..f6888dc28a36 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/source.lua.txt @@ -0,0 +1,3 @@ +local tainted = luci.http.formvaluex("wan") +os.executex(tainted) +os.execute("fixed-command") diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.lua new file mode 100644 index 000000000000..f4e4f8dd30cb --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.lua @@ -0,0 +1,15 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = {} + +function os.execute(command) +end + +local sanitizer = require("sanitizer") +local tainted = luci.http.formvalue("cmd") +local cleaned = sanitizer.clean(tainted) +os.execute(cleaned) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.luac new file mode 100644 index 000000000000..548c2cc8e291 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.lua new file mode 100644 index 000000000000..1665da897d0f --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.lua @@ -0,0 +1,8 @@ +local M = {} + +function M.clean(value) + local cleaned = shellquote(value) + return cleaned +end + +return M diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.luac new file mode 100644 index 000000000000..2f4d8558af35 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.lua.txt new file mode 100644 index 000000000000..ae878dbbea4d --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.lua.txt @@ -0,0 +1,14 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +local mtkwifi = require("mtkwifi") + +function webcmd() + local command_text = luci.http.formvalue("cmd") + return mtkwifi.func_unknow_0_12(command_text) +end + +webcmd() diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.luac new file mode 100644 index 000000000000..494e02b445a4 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.lua.txt new file mode 100644 index 000000000000..f18bd010f4ae --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.lua.txt @@ -0,0 +1,13 @@ +local M = {} + +io = { + popen = function(cmd) + return cmd + end +} + +function M.func_unknow_0_12(command_text) + return io.popen(command_text) +end + +return M diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.luac new file mode 100644 index 000000000000..7fa65fafbfd5 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/input.luac new file mode 100644 index 000000000000..2ccec1b0212f Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/source.lua.txt new file mode 100644 index 000000000000..2b6bd006b7e0 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/source.lua.txt @@ -0,0 +1,14 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = { + execute = function(cmd) + return cmd + end +} + +local command_text = luci.http.formvalue("cmd") +os.execute(command_text) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/input.luac new file mode 100644 index 000000000000..625e984ffefa Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/source.lua.txt new file mode 100644 index 000000000000..694d19ea74da --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/source.lua.txt @@ -0,0 +1,15 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = { + execute = function(cmd) + return cmd + end +} + +local tainted = luci.http.formvalue("cmd") +local clean = "clean" +os.execute(clean) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.lua.txt new file mode 100644 index 000000000000..777191796e49 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.lua.txt @@ -0,0 +1,15 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = {} + +function os.execute(command) +end + +local shellsafe = require("shellsafe") +local tainted = luci.http.formvalue("cmd") +local sanitized = shellsafe.shellquote(tainted) +os.execute(sanitized) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.luac new file mode 100644 index 000000000000..9028a18a10f0 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.lua.txt new file mode 100644 index 000000000000..a183e65ea0b7 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.lua.txt @@ -0,0 +1,7 @@ +local M = {} + +function M.shellquote(value) + return value +end + +return M diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.luac new file mode 100644 index 000000000000..468b5c7f0528 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/input.luac new file mode 100644 index 000000000000..ed5aa4a9e65c Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/source.lua.txt new file mode 100644 index 000000000000..d936c2feb453 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/source.lua.txt @@ -0,0 +1,19 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +local function tonumber(value) + return value +end + +os = { + execute = function(cmd) + return cmd + end +} + +local tainted = luci.http.formvalue("cmd") +local sanitized = tonumber(tainted) +os.execute(sanitized) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/input.luac new file mode 100644 index 000000000000..893ad75a87cc Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/source.lua.txt new file mode 100644 index 000000000000..0d6d879e33f4 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/source.lua.txt @@ -0,0 +1,20 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +local function tonumber(value) + return value +end + +os = { + execute = function(cmd) + return cmd + end +} + +local tainted = luci.http.formvalue("cmd") +local clean = "fixed" +tonumber(clean) +os.execute(tainted) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/input.luac new file mode 100644 index 000000000000..5add3d094321 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/source.lua.txt new file mode 100644 index 000000000000..fce73be1d45d --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/source.lua.txt @@ -0,0 +1,23 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +local function shellquote(value) + return value +end + +os = {} + +function os.execute(command) +end + +local tainted = luci.http.formvalue("cmd") +local command +if condition then + command = shellquote(tainted) +else + command = tainted +end +os.execute(command) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/input.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/input.luac new file mode 100644 index 000000000000..be988c968f7c Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/input.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/source.lua.txt b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/source.lua.txt new file mode 100644 index 000000000000..01d3f7508a05 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/source.lua.txt @@ -0,0 +1,18 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = { + execute = function(cmd) + return cmd + end +} + +function submit_dpp_uri() + local dpp_uri = luci.http.formvalue("dpp_uri") + os.execute(dpp_uri) +end + +submit_dpp_uri() diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.lua new file mode 100644 index 000000000000..5142f3347280 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.lua @@ -0,0 +1,7 @@ +local state = {} + +state.command = source() +if condition() then + state[get_key()] = tonumber(state.command) +end +execute(state.command) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.luac new file mode 100644 index 000000000000..e67b75bab370 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.lua new file mode 100644 index 000000000000..875407b25bb4 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.lua @@ -0,0 +1,7 @@ +local state = {} + +state.command = source() +if condition() then + state.command = tonumber(state.command) +end +execute(state.command) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.luac new file mode 100644 index 000000000000..fcf7b3724f8e Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.lua new file mode 100644 index 000000000000..1e387579b8e8 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.lua @@ -0,0 +1,9 @@ +local state = {} + +state.command = source() +if condition() then + state.command = tonumber(state.command) +else + state.command = shellquote(state.command) +end +execute(state.command) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.luac new file mode 100644 index 000000000000..2a151c81a0dd Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.luac differ diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.lua b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.lua new file mode 100644 index 000000000000..2fa94361c905 --- /dev/null +++ b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.lua @@ -0,0 +1,9 @@ +local state = {} + +state.command = source() +if condition() then + state.other = tonumber(state.command) +else + state.other = shellquote(state.command) +end +execute(state.command) diff --git a/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.luac b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.luac new file mode 100644 index 000000000000..eaf1036ac935 Binary files /dev/null and b/lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.luac differ diff --git a/lua/ql/test/library-tests/README.md b/lua/ql/test/library-tests/README.md new file mode 100644 index 000000000000..89abd2c74bff --- /dev/null +++ b/lua/ql/test/library-tests/README.md @@ -0,0 +1,41 @@ +# Lua test matrix + +All test inputs and exact oracles are committed in their owning directories. +Library tests live under this directory. The +[experimental query tests](../experimental/query-tests) have their own +publication-aligned path. The matrix covers these stable behavior groups: + +| Area | Covered boundary | +| --- | --- | +| Source inventory | `.lua` file identity and metadata without a source-AST claim. | +| Bytecode model | Accepted Lua 5.1 profiles plus malformed and unsupported diagnostics. | +| Local semantics | Control flow, reaching definitions, values, calls, tables, globals, and upvalues. | +| Modules and interprocedural flow | Literal modules, exports, arguments, returns, varargs, open results, and balanced calls. | +| Taint and reports | Active paths, sanitizer paths, negative boundaries, and path alternatives. | +| Query publication | Experimental query metadata, direct execution, active findings, and sanitized review paths. | +| Complex integration | 46 unchanged `.lua` files and 46 stripped Lua 5.1 `.luac` files in a multi-file module layout. | + +Run the complete matrix from the repository root: + +```bash +CODEQL=/absolute/path/to/codeql +ROOT=$(pwd) + +"$CODEQL" test run lua/ql/test \ + --search-path="$ROOT:$ROOT/lua" +``` + +Pass one test directory for a focused review. For example, reproduce the +complex integration oracle with: + +```bash +"$CODEQL" test run \ + lua/ql/test/library-tests/complex-bytecode-integration \ + --search-path="$ROOT:$ROOT/lua" +``` + +The integration [manifest](complex-bytecode-integration/SAMPLE-MANIFEST.md), +[hash inventory](complex-bytecode-integration/SHA256SUMS), and +[compact oracle](complex-bytecode-integration/CorpusAcceptance.expected) are the +complete committed review evidence. Generated databases and reports are not +part of the test corpus. diff --git a/lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.expected b/lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.expected new file mode 100644 index 000000000000..8e2668403e97 --- /dev/null +++ b/lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.expected @@ -0,0 +1,5 @@ +| bc-malformed-diagnostic | malformed-constant | bc-malformed-diagnostic/malformed-constant.luac | +| bc-malformed-diagnostic | not-lua-bytecode | bc-malformed-diagnostic/not-lua-bytecode.luac | +| bc-malformed-diagnostic | truncated-bytecode | bc-malformed-diagnostic/truncated.luac | +| bc-malformed-diagnostic | unsupported-bytecode-profile | bc-malformed-diagnostic/unsupported-profile.luac | +| bc-malformed-diagnostic | unsupported-bytecode-version | bc-malformed-diagnostic/unsupported-version.luac | diff --git a/lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.ql b/lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.ql new file mode 100644 index 000000000000..ede7d4d31a86 --- /dev/null +++ b/lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.ql @@ -0,0 +1,18 @@ +import codeql.lua.Bytecode + +from string fixture, string subject, string detail +where + exists(LuaDiagnostic d | + d.getFixtureId().regexpMatch("bc-malformed-diagnostic/.*") and + fixture = d.getFixtureId().regexpReplaceAll("/.*", "") and + subject = d.getKind() and + detail = d.getInputRef() + ) + or + exists(LuaPrototype p | + p.getFixtureId().regexpMatch("bc-malformed-diagnostic/.*") and + fixture = p.getFixtureId().regexpReplaceAll("/.*", "") and + subject = "unexpected-accepted-prototype" and + detail = p.getPrototypeId() + ) +select fixture, subject, detail diff --git a/lua/ql/test/library-tests/bytecode-model/BytecodeModel.expected b/lua/ql/test/library-tests/bytecode-model/BytecodeModel.expected new file mode 100644 index 000000000000..bc996331b753 --- /dev/null +++ b/lua/ql/test/library-tests/bytecode-model/BytecodeModel.expected @@ -0,0 +1,6 @@ +| bc-constants-call | root:k0 | string | alpha | +| bc-constants-call | root:k1 | number | 7.0 | +| bc-prototype-params | root | prototype | exists | +| bc-prototype-params | root.0 | parent=root | num_params=2 | +| bc-prototype-params | root@pc0 | instruction | CLOSURE | +| bc-stripped-metadata | root.0 | mapping-state | stripped/unavailable | diff --git a/lua/ql/test/library-tests/bytecode-model/BytecodeModel.ql b/lua/ql/test/library-tests/bytecode-model/BytecodeModel.ql new file mode 100644 index 000000000000..41feb44d24d1 --- /dev/null +++ b/lua/ql/test/library-tests/bytecode-model/BytecodeModel.ql @@ -0,0 +1,66 @@ +import codeql.lua.Bytecode + +from string fixture, string subject, string detail, string value +where + exists(LuaPrototype p | + p.getFixtureId() = "bc-prototype-params/input.luac" and + p.getPrototypeId() = "root" and + fixture = p.getFixtureId().regexpReplaceAll("/.*", "") and + subject = p.getPrototypeId() and + detail = "prototype" and + value = "exists" + ) + or + exists(LuaPrototype p | + p.getFixtureId() = "bc-prototype-params/input.luac" and + p.getPrototypeId() = "root.0" and + p.getParentPrototypeId() = "root" and + fixture = p.getFixtureId().regexpReplaceAll("/.*", "") and + subject = p.getPrototypeId() and + detail = "parent=root" and + value = "num_params=" + p.getNumParams().toString() + ) + or + exists(@lua_instruction instruction, string fixtureId, string prototypeId, int pc | + lua_instructions(instruction, _, fixtureId, prototypeId, pc, "CLOSURE", _, _, _) and + fixtureId = "bc-prototype-params/input.luac" and + prototypeId = "root" and + pc = 0 and + fixture = fixtureId.regexpReplaceAll("/.*", "") and + subject = prototypeId + "@pc" + pc.toString() and + detail = "instruction" and + value = "CLOSURE" + ) + or + exists(LuaConstant c | + c.getFixtureId() = "bc-constants-call/input.luac" and + c.getConstantId() = "root:k0" and + c.getLuaType() = "string" and + c.getValue() = "alpha" and + fixture = c.getFixtureId().regexpReplaceAll("/.*", "") and + subject = c.getConstantId() and + detail = c.getLuaType() and + value = c.getValue() + ) + or + exists(LuaConstant c | + c.getFixtureId() = "bc-constants-call/input.luac" and + c.getConstantId() = "root:k1" and + c.getLuaType() = "number" and + c.getValue() = "7.0" and + fixture = c.getFixtureId().regexpReplaceAll("/.*", "") and + subject = c.getConstantId() and + detail = c.getLuaType() and + value = c.getValue() + ) + or + exists(LuaPrototype p | + p.getFixtureId() = "bc-stripped-metadata/input.luac" and + p.getPrototypeId() = "root.0" and + p.getMappingState() = "stripped/unavailable" and + fixture = p.getFixtureId().regexpReplaceAll("/.*", "") and + subject = p.getPrototypeId() and + detail = "mapping-state" and + value = p.getMappingState() + ) +select fixture, subject, detail, value diff --git a/lua/ql/test/library-tests/bytecode-model/bc-constants-call/input.luac b/lua/ql/test/library-tests/bytecode-model/bc-constants-call/input.luac new file mode 100644 index 000000000000..829baa29a56c Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-constants-call/input.luac differ diff --git a/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac new file mode 100644 index 000000000000..6d7172f904f0 Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac differ diff --git a/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac new file mode 100644 index 000000000000..af3f833d8afd --- /dev/null +++ b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac @@ -0,0 +1 @@ +not lua bytecode diff --git a/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/truncated.luac b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/truncated.luac new file mode 100644 index 000000000000..e6835c0d0272 Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/truncated.luac differ diff --git a/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac new file mode 100644 index 000000000000..8b6872a7b18f Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac differ diff --git a/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac new file mode 100644 index 000000000000..29d54d5f8578 Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac differ diff --git a/lua/ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac b/lua/ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac new file mode 100644 index 000000000000..8bce7bc47c7e Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac differ diff --git a/lua/ql/test/library-tests/bytecode-model/bc-stripped-metadata/input.luac b/lua/ql/test/library-tests/bytecode-model/bc-stripped-metadata/input.luac new file mode 100644 index 000000000000..7cd65439088b Binary files /dev/null and b/lua/ql/test/library-tests/bytecode-model/bc-stripped-metadata/input.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/.gitattributes b/lua/ql/test/library-tests/complex-bytecode-integration/.gitattributes new file mode 100644 index 000000000000..5d040edd40db --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/.gitattributes @@ -0,0 +1,2 @@ +lib/** -text +usr/** -text diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.expected b/lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.expected new file mode 100644 index 000000000000..bd58b8aa2471 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.expected @@ -0,0 +1,19 @@ +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc180:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc188:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc196:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc204:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc212:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc220:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc63:r6 | usr/lib/lua/luci/controller/mtkwifi.luac::root.61@pc228:r12 | +| active | usr/lib/lua/luci/controller/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.110@pc3:r0 | usr/lib/lua/luci/controller/mtkwifi.luac::root.110@pc12:r1 | +| active | usr/lib/lua/mtkwifi.luac | usr/lib/lua/luci/controller/mtkwifi.luac::root.55@pc3:r0 | usr/lib/lua/mtkwifi.luac::root.12@pc5:r6 | +| metric | accepted-artifacts | 46 | | +| metric | active-findings | 9 | | +| metric | active-projections-complete | 1 | | +| metric | bytecode-artifacts | 46 | | +| metric | cross-module-flow-present | 1 | | +| metric | diagnostics | 0 | | +| metric | interprocedural-analysis-present | 1 | | +| metric | module-analysis-present | 1 | | +| metric | sanitized-findings | 0 | | +| metric | source-files | 46 | | diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.ql b/lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.ql new file mode 100644 index 000000000000..9e2f017df0fc --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.ql @@ -0,0 +1,67 @@ +import codeql.lua.SourceFile +import codeql.lua.RulesSanitizerReport + +from string rowKind, string first, string second, string third +where + rowKind = "metric" and + third = "" and + ( + first = "source-files" and + second = count(LuaSourceFile file | any()).toString() + or + first = "bytecode-artifacts" and + second = count(LuaArtifact artifact | any()).toString() + or + first = "accepted-artifacts" and + second = count(LuaArtifact artifact | artifact.isAccepted()).toString() + or + first = "diagnostics" and + second = count(LuaDiagnostic diagnostic | any()).toString() + or + first = "active-findings" and + second = + count(LuaFlowNode source, LuaFlowNode sink | activeReportPath(source, sink, _, _, _)) + .toString() + or + first = "sanitized-findings" and + second = + count(LuaFlowNode source, LuaFlowNode sink | sanitizedReportPath(source, sink)).toString() + or + first = "module-analysis-present" and + second = "1" and + exists(string fixture, string modulePath, string moduleName, string provenance | + moduleIdentity(fixture, modulePath, moduleName, provenance) + ) + or + first = "interprocedural-analysis-present" and + second = "1" and + exists(LuaInterproceduralFlow flow | flow.getSourceRef() != "") + or + first = "cross-module-flow-present" and + second = "1" and + exists(string sourceModule, string sourceRef, string sinkModule, string sinkRef | + sourceModule != sinkModule and + genericFlowStep(sourceModule, sourceRef, sinkModule, sinkRef, _, _) + ) + or + first = "active-projections-complete" and + second = "1" and + not exists(LuaFlowNode source, LuaFlowNode sink | + activeReportPath(source, sink, _, _, _) and + ( + source.getModulePath() = "" or + source.getValueRef() = "" or + sink.getModulePath() = "" or + sink.getValueRef() = "" + ) + ) + ) + or + exists(LuaFlowNode source, LuaFlowNode sink | + activeReportPath(source, sink, _, _, _) and + rowKind = "active" and + first = sink.getModulePath() and + second = source.toString() and + third = sink.toString() + ) +select rowKind, first, second, third diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/SAMPLE-MANIFEST.md b/lua/ql/test/library-tests/complex-bytecode-integration/SAMPLE-MANIFEST.md new file mode 100644 index 000000000000..e89a3d255b7a --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/SAMPLE-MANIFEST.md @@ -0,0 +1,20 @@ +# Complex Lua 5.1 Bytecode Integration Corpus + +This test corpus exercises the Lua extractor, bytecode model, module and +interprocedural analysis, and public command-flow queries over a complex +multi-file layout. + +The corpus contains: + +- 46 Lua source files; +- 46 corresponding Lua 5.1 bytecode files; +- 42 source/bytecode pairs under `usr/lib/lua`; +- 4 source/bytecode pairs under `lib/wifi`. + +The source files retain their original bytes and relative paths. The paired +bytecode files are reproducibly compiled with Lua 5.1 debug metadata stripped; +their instruction streams, constants, and prototype structure are unchanged. +The corpus retains its Git-representable executable classification: 13 files +are executable and 79 are non-executable. `SHA256SUMS` records the complete +input inventory. Generated databases, reports, BQRS, CSV, SARIF, logs, and +profiles are not part of the test corpus. diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/SHA256SUMS b/lua/ql/test/library-tests/complex-bytecode-integration/SHA256SUMS new file mode 100644 index 000000000000..6876c5cb0813 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/SHA256SUMS @@ -0,0 +1,92 @@ +ca54fd5b367862e9487303a7827f3f063bb90c1d6ff2b2dcef30691ca6042873 lib/wifi/mtwifi.lua +d7f9e2935c242a06e22a925d2c7396074b5785da471c4356d1a39e851cd3ccb1 lib/wifi/mtwifi.luac +7f5468233596d853437cfd1b1f708d6b7bfb0b5aa960ab29a0d4c77a3b804e1a lib/wifi/quick_setting.lua +01c96d20b5e9bc4e7cac63ebc3db2a2a1d98675a46a44a8d9ba8029e5d391dca lib/wifi/quick_setting.luac +ca73086eb479997f4554efe7b583ce181ddc78ddace9a57075d23655dbab729c lib/wifi/wifi_services.lua +8d2a1f8f35abdf563fb8b073d5d48e7e0d9bdfd6fcce63a1fc731da62100ec84 lib/wifi/wifi_services.luac +080cad4acc64b315764cfdb3c32442ab099f47d00b1a62170913c6d09bf74021 lib/wifi/wifi_services_sh.lua +1401dc8e305a1b976bbd379a40595c64c9765c48565e399edf04aa11f91892bc lib/wifi/wifi_services_sh.luac +e998dbe441b9fe9822596a1cbd7503b446a749c36d0c362b79ed1f78604ababb usr/lib/lua/l1dat_parser.lua +41cad1741fdfcb4e26cdeb80634e9ce4f66bc5f5c4c797b7df6027927a4954c2 usr/lib/lua/l1dat_parser.luac +f8ebb246919c73c9fe1c12a0a8fb0f1124835a0bd56a55df02163f96d664f795 usr/lib/lua/ltn12.lua +71c9f83b681b8c2ba87f34b72959f5be8cb0c1836d007aed2db87906bf570a9d usr/lib/lua/ltn12.luac +2b56a94288a7184bf13a96fe19320b3dad33741982dc285ab1d444ee26ce0f40 usr/lib/lua/luci/cacheloader.lua +e9675ea2416eaa7bc38a0cf7053bfaea455f5c690f5c4b5b7025bc59e7a13f0b usr/lib/lua/luci/cacheloader.luac +0e5244c03f049892ad2688f53eaa0d294a6554c844da7ed01ce9299ae74fd72e usr/lib/lua/luci/ccache.lua +5adbb2f99c38c63037e1c16f0d4fd916e35a33f8db689ca7e72d22ee84d4730a usr/lib/lua/luci/ccache.luac +d3a8e1d0b7513ccef7f01e14d2e47a77c63c0041dc7ea282c299c4ddc777b391 usr/lib/lua/luci/config.lua +39cef586e597a273df76cf501b3a6e7e10566be01610a7539af915db59ad08d6 usr/lib/lua/luci/config.luac +969034d535b70c63d3aba2c033865f764a715279f376434734bd0985acc7afcf usr/lib/lua/luci/controller/admin/index.lua +3d9429952ca368718c6b331b16bd9062c015f2ff2b555d7aa0e8a056b714c3ed usr/lib/lua/luci/controller/admin/index.luac +7bccd253dfee8c017d692f3c56a843fbd0c8510dbb89ea3f73583dd6f42b0ecf usr/lib/lua/luci/controller/admin/uci.lua +15d9454bb89875eb52957ed5010f63a21bb39a9a1b922c98fca356eb8de02406 usr/lib/lua/luci/controller/admin/uci.luac +986d87cf6ebd5bb9198de54caf0223e3706caf407314051019532fa44a414cc4 usr/lib/lua/luci/controller/hwnat.lua +3a1b90d55676b268553cd11e0c7688c459a3cef1d42ec826a59ffad9ee093f6a usr/lib/lua/luci/controller/hwnat.luac +de7c51ab59eb66f0e010c1b06f17f69a1cee922d926178ebd59d37dbf1111e08 usr/lib/lua/luci/controller/ipsec.lua +ad0b8cc09ff324aa7525a18c6ba0e4edf3b1bc11f7666db39a5a8e066e68385f usr/lib/lua/luci/controller/ipsec.luac +38e2370ee39158e02dbcfb90a3f86b68eda5ad48b606effe7f7ca99d0273e732 usr/lib/lua/luci/controller/mtkwifi.lua +8c729d0b6886dc0084656d3bd8c5cb884b77bdcac7bef0a17bcbf9da1363d1ba usr/lib/lua/luci/controller/mtkwifi.luac +88ad34913f1ae4cdea50a890bdb4f865caa3278181578d514c13081dcde53118 usr/lib/lua/luci/debug.lua +b7bbd0006046daef7fc7c7abccdf27ddac987f482dcdcfacd026a8fe3f4c5285 usr/lib/lua/luci/debug.luac +3d9dd293edea1427ceb3db177e4545eb0b343193d9f6fcf2015604095b4205cd usr/lib/lua/luci/dispatcher.lua +85e216a85a995ca605ab1732a6f2aaf720fd5d0ee3085c86d3e456a2277d0d09 usr/lib/lua/luci/dispatcher.luac +ef08bcc4ab70437535754a14e6e6e40303b90800705c61a9cfd7850efb54278c usr/lib/lua/luci/http.lua +3d28e49d77fda6d98be6a41f96a938a0c4c397151d32154db91439a078d33414 usr/lib/lua/luci/http.luac +b966d9099b9352905a4995003277d6fffd68f5bf5951e8936f14ce966bf27af0 usr/lib/lua/luci/i18n.lua +5f1a69a96af6ebbba0f3a0ba7a7fbc33b6422980132cb24c6ff6bc0e6d6b1101 usr/lib/lua/luci/i18n.luac +8d065980a633e648ffa9058105c83e99498da29d79db2af0c373344ff36731c6 usr/lib/lua/luci/ltn12.lua +a0d180a80a50421f86146c5dec3903cafed95be314b1e1eb08bd83f0d33ef2e0 usr/lib/lua/luci/ltn12.luac +7a0a8a3af0b6043a21e0855c985d0c6923210c5b2d8fabc5dab461653f16a968 usr/lib/lua/luci/model/cbi/hwnat.lua +c678f3c81a95bef0c3b17de6f458082692c77efa5630c379ddec6a2fcf318694 usr/lib/lua/luci/model/cbi/hwnat.luac +71f28209d480a3633aa96c9b207f8a9aaae0a16fbb65598764a3b723d5f7c74f usr/lib/lua/luci/model/cbi/ipsec.lua +ed8bb8201cbcad4341df2c2c53a62a2886cfb046075580827bf1340298a20187 usr/lib/lua/luci/model/cbi/ipsec.luac +f1d4bcc5eb79a7b30998285ffe16fc564d53744e2f9cc3f2609dd6e2f8b0d3d2 usr/lib/lua/luci/model/uci.lua +57d9d32e915e5a9d9da3f01bc4b6bbfd1a90f24b48db8190283ca80f451c2aab usr/lib/lua/luci/model/uci.luac +f16bf5f894cdfcaaca514054dec61b79d84b177d609a7f839102b48d1df2451f usr/lib/lua/luci/sgi/cgi.lua +6368c72a52bde6655d2e10f26952c6b4eefd4ad20db3c93ec9ed69066fb7897f usr/lib/lua/luci/sgi/cgi.luac +e546634acaf18ea5a19eb4418bfaafc497eb3ebeddc592ccac145615d4f8f707 usr/lib/lua/luci/sgi/uhttpd.lua +e3a2227f147ce11438520455c65cc993632e20fad9e17509e00df26d7b73f184 usr/lib/lua/luci/sgi/uhttpd.luac +d143232ec252d72d0aa570c35c9c8a0788d42db276ca1c6b7fe27c9146f1e9e0 usr/lib/lua/luci/store.lua +f1248bcced214630b206a2c8b6b9595548dcfd43fdaf689c2564f6fe35fe062a usr/lib/lua/luci/store.luac +a4726234bccd5c05e47bd8a4749722ffc08be8f1c7309106a8fa165dca82cf75 usr/lib/lua/luci/sys.lua +3c7fcc1f33a21cf294ecb2c9cd81bd127a430383d55e49bd99621a8a79c5ff73 usr/lib/lua/luci/sys.luac +2a99a9ab20b9794ec83f0370603c45a0324464f92b59adf4ac16fcb45869c79f usr/lib/lua/luci/sys/zoneinfo.lua +47f87046d4a85bcff3c0dd5fa73cc1ec105aac991e379bdfd4879f1b93c6cd43 usr/lib/lua/luci/sys/zoneinfo.luac +cd0aa5ec7ad9404c8213d71a9ffc2a518165e72a949c92290ee8c98f3069770f usr/lib/lua/luci/sys/zoneinfo/tzdata.lua +31db31cacece58e86f1dbc8b0ee1ea8c2fedb25ecfe8edecaeb6396bd5fcc781 usr/lib/lua/luci/sys/zoneinfo/tzdata.luac +79a27ee15c0cd8eca860d75b86d656be5ddd86f917c6666348e1fef73f4ebdd8 usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua +60c3efd86181201443a97733d04a9ca0a51729c60cf3d6f6c414eeafbba1af89 usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac +e9a590bf485f621370b30fe9d16f031acd2ed71a31a0433b986a5628187336b4 usr/lib/lua/luci/template.lua +fb1c4e8f2e0eb31770870f296a7c67496bb501a150b7cbec8f2bd14260274b5b usr/lib/lua/luci/template.luac +a11075d9516ebcd28d88cab46d3168b08d49418a007e84fb87dda03bc8eac49c usr/lib/lua/luci/util.lua +d9752c5cb0fd285611aefb0b1f0addaf406c44badaaaf23dd946def4a6717f90 usr/lib/lua/luci/util.luac +c75b9a5eee05c4c17386398f2b6241a8fbb63546865323c3c7b668b080c5a038 usr/lib/lua/luci/version.lua +196b4c06e22f4dee084cc7f41df11d71cd16c73ab770d4f557a7ec99bc4628e3 usr/lib/lua/luci/version.luac +ee153d9ec0681d3de1b07d126576eec8ed0a9207ae9322061b23e3d19deeaf0f usr/lib/lua/luci/xml.lua +e2242b1772d7ffdc717a4f1f9f414ad56e78e205a7f0068d1eea6a0a77b2665b usr/lib/lua/luci/xml.luac +922ca4eb2baf9e24fdf6177027bca8e569563608748d1d658a20d7bfa2fd3c2a usr/lib/lua/mime.lua +20c5fe3b2105aca7fb220b0e7b76f814ec89f6e78fe39db7d1750a45c0e2d2f6 usr/lib/lua/mime.luac +9f7273068641b54c44e105df6a3dd49214fd21d65791891d72cafdc0bf16a689 usr/lib/lua/mtkwifi.lua +3fad14c3c7ca8ec3b69791ded570971226769bfee117bcdeebb6e7c3247ed194 usr/lib/lua/mtkwifi.luac +ea07ea6b7b2585afbec439e91a2222aa8cf0a9511fddcea88d04b732ee698678 usr/lib/lua/nixio/fs.lua +3adac4597cab16a3af5997d3f40bbe38b4d03a214d780a454fb0e989b20c156a usr/lib/lua/nixio/fs.luac +1e9a1693ac0b4cb3b4d3e6adaa77279da1dc247182cc6bf2d1f731b30dcfdb9b usr/lib/lua/nixio/util.lua +78f3e954c66cebfc0f69330ad0a18814d7409bace216575217a12ed7e00daa73 usr/lib/lua/nixio/util.luac +0f44a5942a5755501bc066bec64cecd67b68024203d8357389742c6f49ec0a51 usr/lib/lua/shuci.lua +19a5304bb987da548d15653d27abf5cc0b3cfcf9c3db642f54d2f63350e35b10 usr/lib/lua/shuci.luac +a92ae132ce092dc5b8e164ccbc7c737f987b8bbbd481b531db9b64d6c2be4e11 usr/lib/lua/socket.lua +38ee7e5e94dbb9d42c7a6fe6f0a42692656ea0198bfbc40346dc6263cfb6d020 usr/lib/lua/socket.luac +f874ae11becdef5ad36488a2b2e7eb330443212be37d8170536938c9caf83f64 usr/lib/lua/socket/ftp.lua +d21c1632d4adf1799770edec23afa1453afbd54dfaba79cca08d569485fff8f1 usr/lib/lua/socket/ftp.luac +aecff8c2d99d77a6473269367d5b085ad0db189d9699c2f47dcb97ae5ad348df usr/lib/lua/socket/headers.lua +d063058c72d351227488be6715bc448fd3e0bf850d775504a4fc277efa95b95a usr/lib/lua/socket/headers.luac +bd3bb6b71c3f4a925be4b16b23dac10b750ed3943378c4e2191ed19926767ded usr/lib/lua/socket/http.lua +22099b1e602e0b7b5a11d45620c54dd73ed1c155e828f72d06bbd66baec10046 usr/lib/lua/socket/http.luac +55bdeff392dffc381b1487b6e9493a7aadb52f851f78ec29316decf50d88df58 usr/lib/lua/socket/smtp.lua +c0a8e60fd93a58e70f844f2053bea9d5b776b5704809b72e414c352996bc01ba usr/lib/lua/socket/smtp.luac +3ab4f0fc85807c1b1c0b329ff82c74f8dccf9b2a0eac4f2571956641e890c149 usr/lib/lua/socket/tp.lua +d8a4d59e9989ea9493b302194a37d7e9c0aaf5fcfc6ebc8aba24d80097fcd1b3 usr/lib/lua/socket/tp.luac +50c742a3e7b9989e3b2502e81845e4818360ae3d648895a8875cc14008436ab7 usr/lib/lua/socket/url.lua +e3e98a1229661c00410f7d04ae48b599a5c9b45e0f9c2951b92972929a6ac51b usr/lib/lua/socket/url.luac +097470215a9d716a0b9a027851b84d606f03d65f620fcf0d965b5799a2703b22 usr/lib/lua/wps_action.lua +7c80cdb11a9a42f9814e4e2116dd501e27313542d043af89b5aa4d6068dd75a2 usr/lib/lua/wps_action.luac diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.lua b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.lua new file mode 100755 index 000000000000..1840d9802f3f --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.lua @@ -0,0 +1,386 @@ +#!/usr/bin/lua +-- Alternative for OpenWrt's /sbin/wifi. +-- Copyright Not Reserved. +-- Hua Shao + +package.path = '/lib/wifi/?.lua;'..package.path + +local function esc(x) + return (x:gsub('%%', '%%%%') + :gsub('^%^', '%%^') + :gsub('%$$', '%%$') + :gsub('%(', '%%(') + :gsub('%)', '%%)') + :gsub('%.', '%%.') + :gsub('%[', '%%[') + :gsub('%]', '%%]') + :gsub('%*', '%%*') + :gsub('%+', '%%+') + :gsub('%-', '%%-') + :gsub('%?', '%%?')) +end + +function add_vif_into_lan(vif) + local mtkwifi = require("mtkwifi") + local brvifs = mtkwifi.__trim( mtkwifi.read_pipe("uci get network.lan.ifname")) + + if not string.match(brvifs, esc(vif)) then + nixio.syslog("debug", "add "..vif.." into lan") + brvifs = brvifs.." "..vif + --os.execute("uci set network.lan.ifname=\""..brvifs.."\"") --netifd will down vif form /etc/config/network + --os.execute("uci commit") + --os.execute("ubus call network.interface.lan add_device \"{\\\"name\\\":\\\""..vif.."\\\"}\"") + os.execute("brctl addif br-lan "..vif) -- double insurance for rare failure + if mtkwifi.exists("/proc/sys/net/ipv6/conf/"..vif.."/disable_ipv6") then + os.execute("echo 1 > /proc/sys/net/ipv6/conf/"..vif.."/disable_ipv6") + end + else + nixio.syslog("debug", vif.." is already added into lan") + end + brvifs = string.split(mtkwifi.__trim((mtkwifi.read_pipe("ls /sys/class/net/br-lan/brif/")))) + for _,vif in ipairs(brvifs) do + nixio.syslog("debug", "brvif = "..vif) + end +end + +function del_vif_from_lan(vif) + local mtkwifi = require("mtkwifi") + local brvifs = mtkwifi.__trim(mtkwifi.read_pipe("uci get network.lan.ifname")) + if string.match(brvifs, esc(vif)) then + brvifs = mtkwifi.__trim(string.gsub(brvifs, esc(vif), "")) + nixio.syslog("debug", "del "..vif.." from lan") + --os.execute("uci set network.lan.ifname=\""..brvifs.."\"") + --os.execute("uci commit") + --os.execute("ubus call network.interface.lan remove_device \"{\\\"name\\\":\\\""..vif.."\\\"}\"") + if mtkwifi.exists("/proc/sys/net/ipv6/conf/"..vif.."/disable_ipv6") then + os.execute("echo 0 > /proc/sys/net/ipv6/conf/"..vif.."/disable_ipv6") + end + os.execute("brctl delif br-lan "..vif) + end +end + +function mtwifi_up(devname) + local nixio = require("nixio") + local mtkwifi = require("mtkwifi") + local wifi_services_exist = false + if mtkwifi.exists("/lib/wifi/wifi_services.lua") then + wifi_services_exist = require("wifi_services") + end + + if devname then + local profiles = mtkwifi.search_dev_and_profile() + local path = profiles[devname] + if not mtkwifi.exists("/tmp/mtk/wifi/"..string.match(path, "([^/]+)\.dat")..".last") then + os.execute("cp -f "..path.." "..mtkwifi.__profile_previous_settings_path(path)) + end + end + + nixio.syslog("debug", "mtwifi called!") + + local devs, l1parser = mtkwifi.__get_l1dat() + -- l1 profile present, good! + if l1parser and devs then + dev = devs.devname_ridx[devname] + if not dev then + nixio.syslog("err", "mtwifi: dev "..devname.." not found!") + return + end + local profile = mtkwifi.search_dev_and_profile()[devname] + local cfgs = mtkwifi.load_profile(profile) + -- we have to bring up main_ifname first, main_ifname will create all other vifs. + if mtkwifi.exists("/sys/class/net/"..dev.main_ifname) then + nixio.syslog("debug", "mtwifi_up: ifconfig "..dev.main_ifname.." up") + if mtkwifi.exists("/etc/init.d/wpad") then + os.execute("/etc/init.d/wpad start") + else + os.execute("ifconfig "..dev.main_ifname.." up") + add_vif_into_lan(dev.main_ifname) + end + if wifi_services_exist then + miniupnpd_chk(devname, dev.main_ifname, true) + end + else + nixio.syslog("err", "mtwifi_up: main_ifname "..dev.main_ifname.." missing, quit!") + return + end + for _,vif in ipairs(string.split(mtkwifi.read_pipe("ls /sys/class/net"), "\n")) + do + -- add apclix-x to br-lan automatically + if string.match(vif, "apcli%a-%d+") then + add_vif_into_lan(vif) + end + if vif ~= dev.main_ifname and + ( string.match(vif, esc(dev.ext_ifname).."[0-9]+") + or (string.match(vif, esc(dev.apcli_ifname).."[0-9]+") and + cfgs.ApCliEnable ~= "0" and cfgs.ApCliEnable ~= "") + or (string.match(vif, esc(dev.wds_ifname).."[0-9]+") and + cfgs.WdsEnable ~= "0" and cfgs.WdsEnable ~= "") + or string.match(vif, esc(dev.mesh_ifname).."[0-9]+")) + then + nixio.syslog("debug", "mtwifi_up: ifconfig "..vif.." up") + if mtkwifi.exists("/etc/init.d/wpad") then + os.execute("/etc/init.d/wpad start") + else + os.execute("ifconfig "..vif.." up") + add_vif_into_lan(vif) + end + if wifi_services_exist and string.match(vif, esc(dev.ext_ifname).."[0-9]+") then + miniupnpd_chk(devname, vif, true) + end + -- else nixio.syslog("debug", "mtwifi_up: skip "..vif..", prefix not match "..pre) + end + end + if wifi_services_exist then + d8021xd_chk(devname, dev.ext_ifname, dev.main_ifname, true) + end + + else nixio.syslog("debug", "mtwifi_up: skip "..devname..", config(l1profile) not exist") + end + + os.execute(" rm -rf /tmp/mtk/wifi/mtwifi*.need_reload") + -- for ax7800 project, close the ra0. + if string.find(dev.profile_path, "ax7800") then + os.execute("ifconfig ra0 down") + end +end + +function mtwifi_down(devname) + local nixio = require("nixio") + local mtkwifi = require("mtkwifi") + local wifi_services_exist = false + if mtkwifi.exists("/lib/wifi/wifi_services.lua") then + wifi_services_exist = require("wifi_services") + end + + nixio.syslog("debug", "mtwifi_down called!") + + -- M.A.N service + if mtkwifi.exists("/etc/init.d/man") then + os.execute("/etc/init.d/man stop") + end + + local devs, l1parser = mtkwifi.__get_l1dat() + -- l1 profile present, good! + if l1parser and devs then + dev = devs.devname_ridx[devname] + if not dev then + nixio.syslog("err", "mtwifi_down: dev "..devname.." not found!") + return + end + if not mtkwifi.exists("/sys/class/net/"..dev.main_ifname) then + nixio.syslog("err", "mtwifi_down: main_ifname "..dev.main_ifname.." missing, quit!") + return + end + os.execute("iwpriv "..dev.main_ifname.." set hw_nat_register=0") + if wifi_services_exist then + d8021xd_chk(devname,dev.ext_ifname,dev.main_ifname) + end + for _,vif in ipairs(string.split(mtkwifi.read_pipe("ls /sys/class/net"), "\n")) + do + if vif == dev.main_ifname + or string.match(vif, esc(dev.ext_ifname).."[0-9]+") + or string.match(vif, esc(dev.apcli_ifname).."[0-9]+") + or string.match(vif, esc(dev.wds_ifname).."[0-9]+") + or string.match(vif, esc(dev.mesh_ifname).."[0-9]+") + then + nixio.syslog("debug", "mtwifi_down: ifconfig "..vif.." down") + os.execute("killall hostapd") + os.execute("ifconfig "..vif.." down") + del_vif_from_lan(vif) + -- else nixio.syslog("debug", "mtwifi_down: skip "..vif..", prefix not match "..pre) + end + end + else nixio.syslog("debug", "mtwifi_down: skip "..devname..", config not exist") + end + + os.execute(" rm -rf /tmp/mtk/wifi/mtwifi*.need_reload") +end + +function mtwifi_reload(devname) + local nixio = require("nixio") + local mtkwifi = require("mtkwifi") + local normal_reload = true + local qsetting = false + local path, profiles + local devs, l1parser = mtkwifi.__get_l1dat() + nixio.syslog("debug", "mtwifi_reload called!") + + if mtkwifi.exists("/lib/wifi/quick_setting.lua") then + qsetting = true + profiles = mtkwifi.search_dev_and_profile() + end + + -- For one card , all interface should be down, then up + if not devname then + for devname, dev in pairs(devs.devname_ridx) do + mtwifi_down(devname) + end + for devname, dev in mtkwifi.__spairs(devs.devname_ridx) do + if qsetting then + -- Create devname.last for quick setting + path = profiles[devname] + if not mtkwifi.exists("/tmp/mtk/wifi/"..string.match(path, "([^/]+)\.dat")..".applied") then + os.execute("cp -f "..path.." "..mtkwifi.__profile_previous_settings_path(path)) + else + os.execute("cp -f "..mtkwifi.__profile_applied_settings_path(path).. + " "..mtkwifi.__profile_previous_settings_path(path)) + end + end + mtwifi_up(devname) + end + else + if qsetting then + path = profiles[devname] + normal_reload = quick_settings(devname, path) + end + + if normal_reload then + local dev = devs.devname_ridx[devname] + assert(mtkwifi.exists(dev.init_script)) + local compatname = dev.init_compatible + -- Different cards do not affect each other + if not string.find(dev.profile_path, "dbdc") then + if dev.init_compatible == compatname then + mtwifi_down(devname) + mtwifi_up(devname) + end + --If the reloaded device belongs to dbdc, then another device on dbdc also need to be reloaded + else + for devname, dev in pairs(devs.devname_ridx) do + if dev.init_compatible == compatname then + mtwifi_down(devname) + end + end + for devname, dev in mtkwifi.__spairs(devs.devname_ridx) do + if dev.init_compatible == compatname then + mtwifi_up(devname) + end + end + end + end + end + -- for ax7800 project, close the ra0. + if string.find(dev.profile_path, "ax7800") then + os.execute("ifconfig ra0 down") + end +end + +function mtwifi_restart(devname) + local nixio = require("nixio") + local uci = require "luci.model.uci".cursor() + local mtkwifi = require("mtkwifi") + local devs, l1parser = mtkwifi.__get_l1dat() + + -- for AX8400 add 5G interface + local isRoot = false + if devname then + local dev, path, diff + local is7915 = false + dev = devs.devname_ridx[devname] + path = dev.profile_path + is7915 = string.find(path, "mt7915") + diff = mtkwifi.diff_profile(path) + if is7915 and diff.BssidNum then + isRoot = true + end + end + + nixio.syslog("debug", "mtwifi_restart called!") + + -- if wifi driver is built-in, it's necessary action to reboot the device + if mtkwifi.exists("/sys/module/mt_wifi") == false or isRoot then + os.execute("echo reboot_required > /tmp/mtk/wifi/reboot_required") + return + end + + if devname then + local dev = devs.devname_ridx[devname] + assert(mtkwifi.exists(dev.init_script)) + local compatname = dev.init_compatible + for devname, dev in pairs(devs.devname_ridx) do + if dev.init_compatible == compatname then + mtwifi_down(devname) + end + end + else + for devname, dev in pairs(devs.devname_ridx) do + mtwifi_down(devname) + end + end + os.execute("rmmod mt_whnat") + os.execute("/etc/init.d/fwdd stop") + os.execute("rmmod mtfwd") + os.execute("rmmod mtk_warp_proxy") + os.execute("rmmod mtk_warp") + -- mt7915_mt_wifi is for dual ko only + os.execute("rmmod mt7915_mt_wifi") + os.execute("rmmod mt_wifi") + + os.execute("modprobe mt_wifi") + os.execute("modprobe mt7915_mt_wifi") + os.execute("modprobe mtk_warp") + os.execute("modprobe mtk_warp_proxy") + os.execute("modprobe mtfwd") + os.execute("/etc/init.d/fwdd start") + os.execute("modprobe mt_whnat") + if devname then + local dev = devs.devname_ridx[devname] + assert(mtkwifi.exists(dev.init_script)) + local compatname = dev.init_compatible + for devname, dev in mtkwifi.__spairs(devs.devname_ridx) do + if dev.init_compatible == compatname then + mtwifi_up(devname) + end + end + else + for devname, dev in mtkwifi.__spairs(devs.devname_ridx) do + mtwifi_up(devname) + end + end +end + +function mtwifi_reset(devname) + local nixio = require("nixio") + local mtkwifi = require("mtkwifi") + nixio.syslog("debug", "mtwifi_reset called!") + if mtkwifi.exists("/rom/etc/wireless/mediatek/") then + os.execute("rm -rf /etc/wireless/mediatek/") + os.execute("cp -rf /rom/etc/wireless/mediatek/ /etc/wireless/") + mtwifi_reload(devname) + else + nixio.syslog("debug", "mtwifi_reset: /rom"..profile.." missing, unable to reset!") + end +end + +function mtwifi_status(devname) + return wifi_common_status() +end + +function mtwifi_hello(devname) + os.execute("echo mtwifi_hello: "..devname) +end + + +function mtwifi_detect(devname) + local nixio = require("nixio") + local mtkwifi = require("mtkwifi") + nixio.syslog("debug", "mtwifi_detect called!") + + for _,dev in ipairs(mtkwifi.get_all_devs()) do + local relname = string.format("%s%d%d",dev.maindev,dev.mainidx,dev.subidx) + print([[ +config wifi-device ]]..relname.."\n"..[[ + option type mtwifi + option vendor ralink +]]) + for _,vif in ipairs(dev.vifs) do + print([[ +config wifi-iface + option device ]]..relname.."\n"..[[ + option ifname ]]..vif.vifname.."\n"..[[ + option network lan + option mode ap + option ssid ]]..vif.__ssid.."\n") + end + end +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.luac b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.luac new file mode 100644 index 000000000000..317737bc0217 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.lua b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.lua new file mode 100755 index 000000000000..c1ea84a48fa5 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.lua @@ -0,0 +1,1033 @@ + +function debug_info_write(devname,content) + local filename = "/tmp/mtk/wifi/"..devname.."_quick_setting_cmd.sh" + local ff = io.open(filename, "a") + ff:write(content) + ff:write("\n") + ff:close() +end + +function token(str, n, default) + local i = 1 + local list = {} + for k in string.gmatch(str, "([^;]+)") do + list[i] = k + i = i + 1 + end + return list[tonumber(n)] or default +end + +function GetFileSize( filename ) + local fp = io.open( filename ) + if fp == nil then + return nil + end + local filesize = fp:seek( "end" ) + fp:close() + return filesize +end + +function vifs_cfg_parm(parm) + local vifs_cfg_parms = {"AuthMode", "EncrypType", "Key", "WPAPSK", "Access", "^WPS", "^wps", "^Wsc", "PIN", "^WEP", ";", "_"} + for _, pat in ipairs(vifs_cfg_parms) do + if string.find(parm, pat) then + return false + end + end + return true +end + +function __set_wifi_apcli_security(cfgs, diff, device, devname) + -- to keep it simple, we always reconf the security if anything related is changed. + -- do optimization only if there's significant performance defect. + --if not diff[ApCliEnable][2] == "1" and cfgs[ApCliEnable] ~= "1" then return end + local vifs = {} -- changed vifs + + -- figure out which vif is changed + -- since multi-bssid is possible, both AuthMode and EncrypType can be a group + local auth_old = cfgs.ApCliAuthMode:split() or {} + local encr_old = cfgs.ApCliEncrypType and cfgs.ApCliEncrypType:split() or {} + local keyid_old = cfgs.ApCliDefaultKeyID:split() or {} + local auth_old_i = (auth_old[1] or ''):split(";") + local encr_old_i = (encr_old[1] or ''):split(";") + local keyid_old_i = (keyid_old[1] or ''):split(";") + local auth_new = diff.ApCliAuthMode and diff.ApCliAuthMode[2]:split() or auth_old + local auth_new_i = (auth_new[1] or ''):split(";") + local encr_new = diff.ApCliEncrypType and diff.ApCliEncrypType[2]:split() or encr_old + local encr_new_i = (encr_new[1] or ''):split(";") + local keyid_new = diff.ApCliDefaultKeyID and diff.ApCliDefaultKeyID[2]:split() or keyid_old + local keyid_new_i = (keyid_new[1] or ''):split(";") + + + --print("encry ="..encr_new[1],auth_new[1], keyid_new[1],keyid_new_i[1]) + --print("encry_old ="..encr_old[1],auth_old[1], keyid_old[1]) + local num = math.max(#encr_old_i, #encr_new_i) + for i = 1, num do + local changed = false + if next(auth_new) and auth_old_i[i] ~= auth_new_i[i] then + changed = true + elseif next(encr_new) and encr_old_i[i] ~= encr_new_i[i] then + changed = true + elseif next(keyid_new) and keyid_old_i[i] ~= keyid_new_i[i] then + changed = true + elseif diff["ApCliWPAPSK"] then + changed = true + else + -- just support apcli0/apclii0/apclix0 + for j = 1, 4 do + if diff["ApCliKey"..tostring(j).."Str"] then + changed = true + break + end + end + end + + if changed then + local vif = {} + vif.idx = i + vif.vifname = device.apcli_ifname..tostring(i-1) + vif.AuthMode = auth_new_i and auth_new_i[i] or auth_old_i[i] + vif.EncrypType = encr_new_i and encr_new_i[i] or encr_old_i[i] + vif.KeyID = keyid_new_i and keyid_new_i[i] or keyid_old_i[i] + vif.DefaultKeyID_idx = "ApCliKey"..tostring(vif.KeyID) + vif.DefaultKey = diff["ApCliKey"..tostring(vif.KeyID).."Str"] + and diff["ApCliKey"..tostring(vif.KeyID).."Str"][2] or cfgs["ApCliKey"..tostring(vif.KeyID).."Str"] + --vif.WEPType = "WEP"..tostring(vif.KeyID).."Type" + --vif.WEPTypeVal = diff["WEP"..tostring(vif.KeyID).."Type"..tostring(i)] and + --diff["WEP"..tostring(vif.KeyID).."Type"..tostring(i)][2] or cfgs["WEP"..tostring(vif.KeyID).."Type"..tostring(i)] + vif.WPAPSK = diff["ApCliWPAPSK"] and diff["ApCliWPAPSK"][2] or cfgs["ApCliWPAPSK"] + vif.SSID = diff["ApCliSsid"] and diff["ApCliSsid"][2] or cfgs["ApCliSsid"] + table.insert(vifs, vif) + end + end + + -- iwpriv here + for i, vif in ipairs(vifs) do + if vif.AuthMode == "OPEN" then + if vif.EncrypType == "WEP" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=OPEN; + iwpriv %s set ApCliEncrypType=WEP; + iwpriv %s set %s="%s"; + iwpriv %s set ApCliDefaultKeyID=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.DefaultKeyID_idx, + vif.DefaultKey, vif.vifname, vif.KeyID, vif.vifname) + else + commands = string.format([[ + iwpriv %s set ApCliAuthMode=OPEN; + iwpriv %s set ApCliEncrypType=NONE; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname) + end + elseif vif.AuthMode == "WEPAUTO" and vif.EncrypType == "WEP" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=OPEN; + iwpriv %s set ApCliEncrypType=WEP; + iwpriv %s set %s="%s"; + iwpriv %s set ApCliDefaultKeyID=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.DefaultKeyID_idx, + vif.DefaultKey, vif.vifname, vif.KeyID, vif.vifname) + elseif vif.AuthMode == "OWE" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=OWE; + iwpriv %s set ApCliEncrypType=AES; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname) + elseif vif.AuthMode == "SHARED" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=SHARED; + iwpriv %s set ApCliEncrypType=WEP; + iwpriv %s set %s="%s"; + iwpriv %s set ApCliDefaultKeyID=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.DefaultKeyID_idx, + vif.DefaultKey, vif.vifname, vif.KeyID, vif.vifname) + elseif vif.AuthMode == "WPA2PSK" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=WPA2PSK; + iwpriv %s set ApCliEncrypType=%s; + iwpriv %s set ApCliWPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPA3PSK" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=WPA3PSK; + iwpriv %s set ApCliEncrypType=%s; + iwpriv %s set ApCliWPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPAPSKWPA2PSK" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=WPAPSKWPA2PSK; + iwpriv %s set ApCliEncrypType=%s; + iwpriv %s set ApCliWpaMixPairCipher=WPA_TKIP_WPA2_AES; + iwpriv %s set ApCliWPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPA2PSKWPA3PSK" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=WPA2PSKWPA3PSK; + iwpriv %s set ApCliEncrypType=%s; + iwpriv %s set ApCliRekeyMethod=TIME; + iwpriv %s set ApCliWPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPAPSK" then + commands = string.format([[ + iwpriv %s set ApCliAuthMode=WPAPSK; + iwpriv %s set ApCliEncrypType=%s; + iwpriv %s set ApCliWPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPA1WPA2" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA1WPA2; + iwpriv %s set EncrypType=%s; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.RADIUS_Server, + vif.vifname,vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname) + else + error(string.format("invalid AuthMode \"%s\"", vif.AuthMode)) + end + + -- must append extra SSID command to make changes take effect + commands = commands .."\n".. string.format([[ + iwpriv %s set ApCliSSID="%s";]], vif.vifname, vif.SSID) + debug_info_write(devname, commands) + end +end + +function __set_wifi_security(cfgs, diff, device, devname) + -- to keep it simple, we always reconf the security if anything related is changed. + -- do optimization only if there's significant performance defect. + + local vifs = {} -- changed vifs + + -- figure out which vif is changed + -- since multi-bssid is possible, both AuthMode and EncrypType can be a group + local auth_old = cfgs.AuthMode:split() + local encr_old = cfgs.EncrypType:split() + local IEEE8021X_old = cfgs.IEEE8021X:split() + local keyid_old = cfgs.DefaultKeyID:split() + local auth_new = {} + local auth_new1 = {} + local encr_new = {} + local encr_new1 = {} + local IEEE8021X_new ={} + local IEEE8021X_new1 ={} + local keyid_new = {} + local keyid_new1 = {} + + if diff.EncrypType then + encr_new = diff.EncrypType[2]:split() + encr_new1 = encr_new[1]:split(";") + end + if diff.AuthMode then + auth_new = diff.AuthMode[2]:split() + auth_new1 = auth_new[1]:split(";") + end + if diff.IEEE8021X then + IEEE8021X_new = diff.IEEE8021X[2]:split() + IEEE8021X_new1 = IEEE8021X_new[1]:split(";") + end + if diff.DefaultKeyID then + keyid_new = diff.DefaultKeyID[2]:split() + keyid_new1 = keyid_new[1]:split(";") + end + + -- For WPA/WPA2 + local RadiusS_old = cfgs.RADIUS_Server:split() or {} + local RadiusP_old = cfgs.RADIUS_Port:split() or {} + local RadiusS_old_i = (RadiusS_old[1] or ''):split(";") + local RadiusP_old_i = (RadiusP_old[1] or ''):split(";") + local RadiusS_new = diff.RADIUS_Server and diff.RADIUS_Server[2]:split() or RadiusS_old + local RadiusP_new = diff.RADIUS_Port and diff.RADIUS_Port[2]:split() or RadiusP_old + local RadiusS_new_i = (RadiusS_new[1] or ''):split(";") --split by ";" + local RadiusP_new_i = (RadiusP_new[1] or ''):split(";") + + local auth_old1 = auth_old[1]:split(";") --auth_old1[1]=OPEN,auth_old1[2]=WPA2PSK + local encr_old1 = encr_old[1]:split(";") + local IEEE8021X_old1 =IEEE8021X_old[1]:split(";") + local keyid_old1 =keyid_old[1]:split(";") + + for i = 1, #encr_old1 do + local changed = false + if next(auth_new) and auth_old1[i] ~= auth_new1[i] then + changed = true + elseif next(encr_new) and encr_old1[i] ~= encr_new1[i] then + changed = true + elseif next(IEEE8021X_new) and IEEE8021X_old1[i] ~= IEEE8021X_new1[i] then + changed = true + elseif next(keyid_new) and keyid_old1[i] ~= keyid_new1[i] then + changed = true + elseif diff["WPAPSK"..tostring(i)] then + changed = true + elseif next(RadiusS_new) and RadiusS_old_i[i] ~= RadiusS_new_i[i] then + changed = true + elseif next(RadiusP_new) and RadiusP_old_i[i] ~= RadiusP_new_i[i] then + changed = true + elseif diff["RADIUS_Key"..tostring(i)] then + changed = true + else + for j = 1, 4 do + if diff["Key"..tostring(j).."Str"..tostring(i)] then + changed = true + break + end + end + end + + if changed then + local vif = {} + vif.idx = i + vif.vifname = device.ext_ifname..tostring(i-1) + vif.ext_ifname = device.ext_ifname + vif.AuthMode = auth_new1 and auth_new1[i] or auth_old1[i] + vif.EncrypType = encr_new1 and encr_new1[i] or encr_old1[i] + vif.KeyID = keyid_new1 and keyid_new1[i] or keyid_old1[i] + vif.DefaultKeyID_idx = "Key"..tostring(vif.KeyID) + vif.DefaultKey = diff["Key"..tostring(vif.KeyID).."Str"..tostring(i)] and + diff["Key"..tostring(vif.KeyID).."Str"..tostring(i)][2] or cfgs["Key"..tostring(vif.KeyID).."Str"..tostring(i)] + vif.WEPType = "WEP"..tostring(vif.KeyID).."Type" + vif.WEPTypeVal = diff["WEP"..tostring(vif.KeyID).."Type"..tostring(i)] and + diff["WEP"..tostring(vif.KeyID).."Type"..tostring(i)][2] or cfgs["WEP"..tostring(vif.KeyID).."Type"..tostring(i)] + vif.WPAPSK = diff["WPAPSK"..tostring(i)] and diff["WPAPSK"..tostring(i)][2] or cfgs["WPAPSK"..tostring(i)] + vif.SSID = diff["SSID"..tostring(i)] and diff["SSID"..tostring(i)][2] or cfgs["SSID"..tostring(i)] + vif.IEEE8021X = IEEE8021X_new1 and IEEE8021X_new1[i] or IEEE8021X_old1[i] + vif.RADIUS_Server = RadiusS_new_i and RadiusS_new_i[i] or RadiusS_old_i[i] + vif.RADIUS_Port = RadiusP_new_i and RadiusP_new_i[i] or RadiusP_old_i[i] + vif.RADIUS_Key = diff["RADIUS_Key"..tostring(i)] and diff["RADIUS_Key"..tostring(i)][2] or cfgs["RADIUS_Key"..tostring(i)] + table.insert(vifs, vif) + end + end + + -- iwpriv here + for i, vif in ipairs(vifs) do + if vif.AuthMode == "OPEN" then + if vif.EncrypType == "WEP" then + commands = string.format([[ + iwpriv %s set AuthMode=OPEN; + iwpriv %s set EncrypType=WEP; + iwpriv %s set %s="%s"; + iwpriv %s set DefaultKeyID=%s; + iwpriv %s set %s=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.DefaultKeyID_idx, vif.DefaultKey, + vif.vifname, vif.KeyID, vif.vifname, vif.WEPType,vif.WEPTypeVal, vif.vifname) + elseif vif.EncrypType == "NONE" and vif.IEEE8021X == "1" then + commands = string.format([[ + iwpriv %s set AuthMode=OPEN; + iwpriv %s set EncrypType=NONE; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=1;]], + vif.vifname, vif.vifname, vif.vifname, vif.RADIUS_Server, + vif.vifname, vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname) + else + commands = string.format([[ + iwpriv %s set AuthMode=OPEN; + iwpriv %s set EncrypType=NONE; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname) + end + elseif vif.AuthMode == "WEPAUTO" and vif.EncrypType == "WEP" then + commands = string.format([[ + iwpriv %s set AuthMode=WEPAUTO; + iwpriv %s set EncrypType=WEP; + iwpriv %s set %s="%s"; + iwpriv %s set DefaultKeyID=%s; + iwpriv %s set %s=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.DefaultKeyID_idx, vif.DefaultKey, + vif.vifname, vif.KeyID, vif.vifname, vif.WEPType, vif.WEPTypeVal, vif.vifname) + elseif vif.AuthMode == "OWE" then + commands = string.format([[ + iwpriv %s set AuthMode=OWE; + iwpriv %s set EncrypType=AES; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname) + elseif vif.AuthMode == "SHARED" then + commands = string.format([[ + iwpriv %s set AuthMode=SHARED; + iwpriv %s set EncrypType=WEP; + iwpriv %s set %s="%s"; + iwpriv %s set DefaultKeyID=%s; + iwpriv %s set %s=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.DefaultKeyID_idx, vif.DefaultKey, + vif.vifname, vif.KeyID, vif.vifname, vif.WEPType,vif.WEPTypeVal, vif.vifname) + elseif vif.AuthMode == "WPA2PSK" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA2PSK; + iwpriv %s set EncrypType=%s; + iwpriv %s set WPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPA3PSK" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA3PSK; + iwpriv %s set EncrypType=%s; + iwpriv %s set WPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPAPSKWPA2PSK" then + commands = string.format([[ + iwpriv %s set AuthMode=WPAPSKWPA2PSK; + iwpriv %s set EncrypType=%s; + iwpriv %s set WpaMixPairCipher=WPA_TKIP_WPA2_AES; + iwpriv %s set WPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPA2PSKWPA3PSK" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA2PSKWPA3PSK; + iwpriv %s set EncrypType=%s; + iwpriv %s set RekeyMethod=TIME; + iwpriv %s set WPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPAPSK" then + commands = string.format([[ + iwpriv %s set AuthMode=WPAPSK; + iwpriv %s set EncrypType=%s; + iwpriv %s set WPAPSK="%s";]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.WPAPSK) + elseif vif.AuthMode == "WPA" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA; + iwpriv %s set EncrypType=%s; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.RADIUS_Server, + vif.vifname, vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname) + elseif vif.AuthMode == "WPA2" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA2; + iwpriv %s set EncrypType=%s; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=0; + 8021xd -p %s -i %s -d 3;]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.RADIUS_Server, + vif.vifname,vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname, vif.ext_ifname, vif.vifname) + elseif vif.AuthMode == "WPA3" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA3; + iwpriv %s set EncrypType=%s; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.RADIUS_Server, + vif.vifname,vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname) + elseif vif.AuthMode == "WPA1WPA2" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA1WPA2; + iwpriv %s set EncrypType=%s; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.EncrypType, vif.vifname, vif.RADIUS_Server, + vif.vifname,vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname) + elseif vif.AuthMode == "WPA3-192" then + commands = string.format([[ + iwpriv %s set AuthMode=WPA3-192; + iwpriv %s set EncrypType=GCMP256; + iwpriv %s set RADIUS_Server=%s; + iwpriv %s set RADIUS_Port=%s; + iwpriv %s set RADIUS_Key=%s; + iwpriv %s set IEEE8021X=0;]], + vif.vifname, vif.vifname, vif.vifname, vif.RADIUS_Server, + vif.vifname,vif.RADIUS_Port, vif.vifname, vif.RADIUS_Key, vif.vifname) + else + error(string.format("invalid AuthMode \"%s\"", vif.AuthMode)) + end + + -- must append extra SSID command to make changes take effect + commands = commands .."\n".. string.format([[ + iwpriv %s set SSID="%s";]], vif.vifname, vif.SSID) + debug_info_write(devname, commands) + end +end + +function __set_he_mu(cfgs, diff, device, devname) + local vifs = {} -- changed vifs + + local mu_of_maDl_old = cfgs.MuOfdmaDlEnable:split() + local mu_of_maUl_old = cfgs.MuOfdmaUlEnable:split() + local mu_mi_moDl_old = cfgs.MuMimoDlEnable:split() + local mu_mi_moUl_old = cfgs.MuMimoUlEnable:split() + local mu_of_maDl_new = {} + local mu_of_maDl_new1 = {} + local mu_of_maUl_new = {} + local mu_of_maUl_new1 = {} + local mu_mi_moDl_new = {} + local mu_mi_moDl_new1 = {} + local mu_mi_moUl_new = {} + local mu_mi_moUl_new1 = {} + + if diff.MuOfdmaDlEnable then + mu_of_maDl_new = diff.MuOfdmaDlEnable[2]:split() + mu_of_maDl_new1 = mu_of_maDl_new[1]:split(";") + end + if diff.MuOfdmaUlEnable then + mu_of_maUl_new = diff.MuOfdmaUlEnable[2]:split() + mu_of_maUl_new1 = mu_of_maUl_new[1]:split(";") + end + if diff.MuMimoDlEnable then + mu_mi_moDl_new = diff.MuMimoDlEnable[2]:split() + mu_mi_moDl_new1 = mu_mi_moDl_new[1]:split(";") + end + if diff.MuMimoUlEnable then + mu_mi_moUl_new = diff.MuMimoUlEnable[2]:split() + mu_mi_moUl_new1 = mu_mi_moUl_new[1]:split(";") + end + + local mu_of_maDl_old1 = mu_of_maDl_old[1]:split(";") + local mu_of_maUl_old1 = mu_of_maUl_old[1]:split(";") + local mu_mi_moDl_old1 = mu_mi_moDl_old[1]:split(";") + local mu_mi_moUl_old1 = mu_mi_moUl_old[1]:split(";") + + for i = 1, #mu_of_maDl_old1 do + local changed = false + if next(mu_of_maDl_new) and mu_of_maDl_old1[i] ~= mu_of_maDl_new1[i] then + changed = true + elseif next(mu_of_maUl_new) and mu_of_maUl_old1[i] ~= mu_of_maUl_new1[i] then + changed = true + elseif next(mu_mi_moDl_new) and mu_mi_moDl_old1[i] ~= mu_mi_moDl_new1[i] then + changed = true + elseif next(mu_mi_moUl_new) and mu_mi_moUl_old1[i] ~= mu_mi_moUl_new1[i] then + changed = true + end + + if changed then + local vif = {} + vif.idx = i + vif.vifname = device.ext_ifname..tostring(i-1) + vif.MuOfdmaDlEnable = mu_of_maDl_new1 and mu_of_maDl_new1[i] or mu_of_maDl_old1[i] + vif.MuOfdmaUlEnable = mu_of_maUl_new1 and mu_of_maUl_new1[i] or mu_of_maUl_old1[i] + vif.MuMimoDlEnable = mu_mi_moDl_new1 and mu_mi_moDl_new1[i] or mu_mi_moDl_old1[i] + vif.MuMimoUlEnable = mu_mi_moUl_new1 and mu_mi_moUl_new1[i] or mu_mi_moUl_old1[i] + vif.SSID = diff["SSID"..tostring(i)] and diff["SSID"..tostring(i)][2] or cfgs["SSID"..tostring(i)] + table.insert(vifs, vif) + end + + end + + -- iwpriv here + for i, vif in ipairs(vifs) do + if vif.MuOfdmaDlEnable == "0" then + commands1 = string.format([[ + iwpriv %s set muru_dl_enable=0;]], + vif.vifname) + elseif vif.MuOfdmaDlEnable == "1" then + commands1 = string.format([[ + iwpriv %s set muru_dl_enable=1;]], + vif.vifname) + else + error(string.format("invalid MuOfdmaDlEnable \"%s\"", vif.MuOfdmaDlEnable)) + end + + if vif.MuOfdmaUlEnable == "0" then + commands2 = string.format([[ + iwpriv %s set muru_ul_enable=0;]], + vif.vifname) + elseif vif.MuOfdmaUlEnable == "1" then + commands2 = string.format([[ + iwpriv %s set muru_ul_enable=1;]], + vif.vifname) + else + error(string.format("invalid MuOfdmaUlEnable \"%s\"", vif.MuOfdmaUlEnable)) + end + + if vif.MuMimoDlEnable == "0" then + commands3 = string.format([[ + iwpriv %s set mu_dl_enable=0;]], + vif.vifname) + elseif vif.MuMimoDlEnable == "1" then + commands3 = string.format([[ + iwpriv %s set mu_dl_enable=1;]], + vif.vifname) + else + error(string.format("invalid MuMimoDlEnable \"%s\"", vif.MuMimoDlEnable)) + end + + if vif.MuMimoUlEnable == "0" then + commands4 = string.format([[ + iwpriv %s set mu_ul_enable=0;]], + vif.vifname) + elseif vif.MuMimoUlEnable == "1" then + commands4 = string.format([[ + iwpriv %s set mu_ul_enable=1;]], + vif.vifname) + else + error(string.format("invalid MuMimoUlEnable \"%s\"", vif.MuMimoUlEnable)) + end + + commands = commands1.."\n"..commands2.."\n" ..commands3.."\n"..commands4.."\n".. string.format([[ + iwpriv %s set SSID="%s";]], vif.vifname, vif.SSID) + debug_info_write(devname, commands) + end +end + +--dev cfg, key is dat parm, value is for iwpriv cmd. +function match_dev_parm(key) + local dat_iw_table = { + CountryCode = "CountryCode", + CountryRegion = "CountryRegion", + CountryRegionABand = "CountryRegionABand", + BGProtection = "BGProtection", + ShortSlot = "ShortSlot", + PktAggregate = "PktAggregate", + HT_BADecline = "BADecline", + HT_DisallowTKIP = "HtDisallowTKIP", + TxPreamble = "TxPreamble", + TxBurst = "TxBurst", + HT_MCS = "HtMcs", + HT_EXTCHA = "HtExtCha", + HT_MpduDensity = "HtMpduDensity", + HT_RDG = "HtRdg", + VOW_Airtime_Fairness_En = "vow_airtime_fairness_en", + HT_TxStream = "HtTxStream", + HT_RxStream = "HtRxStream", + DtimPeriod = "DtimPeriod", + IEEE80211H = "IEEE80211H", + HT_BAWinSize = "HtBaWinSize" + } + + return dat_iw_table[key] +end + +function match_dev_parm_no_ssid(key) + local dat_iw_table = { + BeaconPeriod = "BeaconPeriod", + TxPower = "TxPower", + SREnable = "srcfgsren" + } + + return dat_iw_table[key] +end + +function match_vif_parm(key) + local dat_iw_table = { + APSDCapable = "UAPSDCapable", + FragThreshold = "FragThreshold", + HT_AMSDU = "HtAmsdu", + HT_AutoBA = "HtAutoBa", + HT_GI = "HtGi", + HT_OpMode = "HtOpMode", + HT_PROTECT = "HtProcect", + HT_STBC = "HtStbc", + IgmpSnEnable = "IgmpSnEnable", + NoForwarding = "NoForwarding", + HideSSID = "HideSSID", + WmmCapable = "WmmCapable", + PMKCachePeriod = "PMKCachePeriod", + PreAuth = "PreAuth", + PMFMFPC = "PMFMFPC", + PMFMFPR = "PMFMFPR", + PMFSHA256 = "PMFSHA256", + VHT_STBC = "VhtStbc", + WirelessMode = "WirelessMode", + WscConfMode = "WscConfMode", + WscConfStatus = "WscConfStatus", + VHT_BW_SIGNAL = "VhtBwSignal", + } + + return dat_iw_table[key] +end + +function match_vif_parm_no_ssid(key) + local dat_iw_table = { + RTSThreshold = "RTSThreshold", + } + + return dat_iw_table[key] +end + +function __set_wifi_misc(cfgs, diff, device,devname) + local vifname = device.main_ifname + local vifext = device.ext_ifname + local vifapcli = device.apcli_ifname + local last_command = string.format([[ + iwpriv %s set SSID="%s";]], vifname, cfgs.SSID1) + + local vifidx = cfgs.AuthMode:split(";") + local commands_vifs_ssid = false + local commands_dev = false -- flag for setting ssid + local commands_ssid = {} + local commands_access_1 = {} + local commands_access_2 = {} -- for black list + local commands_bw = false -- for BW, to prevent exexute cmd twice + + for k,v in pairs(diff) do + local commands, commands_1, commands_2, commandns + local commands_vifs, val + if k:find("^SSID") then + local _,_,i = string.find(k, "^SSID([%d]+)") + if i == "1" then + last_command = string.format([[ + iwpriv %s set SSID="%s";]], vifname, tostring(v[2])) + commands_dev = true + else + commands_ssid[tonumber(i)] = string.format([[ + iwpriv %s set SSID="%s";]], vifext..tostring(tonumber(i)-1), tostring(v[2])) + end + ---------------------------------------------------------------------------------------------------- + -----------------------------device config ---------------------------- + elseif k == "Channel" or k == "channel" then + if v[2] == "0" then + cmdchann = string.format([[ + iwpriv %s set AutoChannelSel=3;]], vifname) + else + cmdchann = string.format([[ + iwpriv %s set Channel=%s;]], vifname, tostring(v[2])) + end + debug_info_write(devname, cmdchann) + elseif k == "AutoChannelSelect" then + -- do nothing + elseif k == "PowerUpCckOfdm" or k == "powerupcckOfdm" then + val = "0:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + elseif k == "PowerUpHT20" or k == "powerupht20" then + val = "1:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + elseif k == "PowerUpHT40" or k == "powerupht40" then + val = "2:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + elseif k == "PowerUpVHT20" or k == "powerupvht20" then + val = "3:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + elseif k == "PowerUpVHT40" or k == "powerupvht40" then + local val = "4:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + elseif k == "PowerUpVHT80" or k == "powerupvht80" then + val = "5:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + elseif k == "PowerUpVHT160" or k == "powerupvht160" then + val = "6:"..v[2] + commands = string.format([[ + iwpriv %s set TxPowerBoostCtrl=%s;]], vifname, tostring(val)) + + -- Find k in dat_iw_table and return the iwkey for iwpriv. + elseif match_dev_parm(k) then + commands = string.format([[ + iwpriv %s set %s=%s;]], vifname, tostring(match_dev_parm(k)), tostring(v[2])) + + -- Don't need to set SSID + elseif match_dev_parm_no_ssid(k) then + commandns = string.format([[ + iwpriv %s set %s=%s;]], vifname, tostring(match_dev_parm_no_ssid(k)), tostring(v[2])) + + --Wps need double check + --elseif k == "PINCode" or k == "pincode" then + --commands = string.format([[ + --iwpriv %s set WscVendorPinCode=%s;]], vifname, tostring(v[2])) + --elseif k == "PINPBCRadio" or k == "pinpbcradio" then + --commands = string.format([[ + --iwpriv %s set WscMode=%s;]], vifname, tostring(v[2])) + --elseif k == "PIN" or k == "pin" then + --commands = string.format([[ + --iwpriv %s set WscPinCode=%s;]], vifname, tostring(v[2])) + + -- For apcli + elseif k == "MACRepeaterEn" or k == "maprepeateren" then + commands = string.format([[ + iwpriv %s set MACRepeaterEn=%s;]], vifapcli..tostring(0), tostring(v[2])) + + ---------------------------------------------------------------------------------------------------- + -----------------------------interface config ---------------------------- + -- Common case, set vif parameter and it's ssid + elseif match_vif_parm(k) then + for i=1, #vifidx do + if token(cfgs[k], i) ~= token(diff[k][2], i) then + commands_vifs = string.format([[ + iwpriv %s set %s=%s;]], vifext..tostring(tonumber(i)-1), tostring(match_vif_parm(k)), token(diff[k][2], i)) + commands_ssid[i] = string.format([[ + iwpriv %s set SSID=%s;]], vifext..tostring(tonumber(i)-1), diff["SSID"..tostring(i)] and + tostring(diff["SSID"..tostring(i)][2]) or cfgs["SSID"..tostring(i)]) + debug_info_write(devname, commands_vifs) + end + end + + -- Don't need to set SSID, it will take effect immediately after iwpriv + elseif match_vif_parm_no_ssid(k) then + for i=1, #vifidx do + if token(cfgs[k], i) ~= token(diff[k][2], i) then + commands_vifs = string.format([[ + iwpriv %s set %s=%s;]], vifext..tostring(tonumber(i)-1), tostring(match_vif_parm_no_ssid(k)), token(diff[k][2], i)) + debug_info_write(devname, commands_vifs) + end + end + + -- Special case : need to set multiple parameters at the same time when one parameter changed + elseif k == "RekeyInterval" or k == "rekeyinterval" then + for i=1, #vifidx do + if token(cfgs.RekeyInterval, i) ~= token(diff.RekeyInterval[2], i) then + commands_vifs = string.format([[ + iwpriv %s set RekeyInterval=%s;]], vifext..tostring(tonumber(i)-1), token(diff.RekeyInterval[2], i)) + local commands_time = string.format([[ + iwpriv %s set RekeyMethod=TIME;]], vifext..tostring(tonumber(i)-1)) + commands_ssid[i] = string.format([[ + iwpriv %s set SSID=%s;]], vifext..tostring(tonumber(i)-1), diff["SSID"..tostring(i)] and + tostring(diff["SSID"..tostring(i)][2]) or cfgs["SSID"..tostring(i)]) + debug_info_write(devname, commands_vifs) + debug_info_write(devname, commands_time) + end + end + + -- Special case : need to set multiple parameters at the same time when one parameter changed + elseif not commands_bw and k == "HT_BSSCoexistence" then + for i=1, #vifidx do + commands_vifs = string.format([[ + iwpriv %s set HtBssCoex=%s;]], vifext..tostring(tonumber(i)-1), tostring(v[2])) + commands_1 = string.format([[ + iwpriv %s set HtBw=%s;]], vifext..tostring(tonumber(i)-1), diff["HT_BW"] and tostring(diff["HT_BW"][2]) + or tostring(cfgs["HT_BW"])) + commands_2 = string.format([[ + iwpriv %s set VhtBw=%s;]], vifext..tostring(tonumber(i)-1), diff["VHT_BW"] and tostring(diff["VHT_BW"][2]) + or tostring(cfgs["VHT_BW"])) + if commands_vifs then + debug_info_write(devname, commands_1) + debug_info_write(devname, commands_2) + debug_info_write(devname, commands_vifs) + end + end + commands_vifs_ssid = true + commands_bw = true + elseif not commands_bw and k == "HT_BW" then + local htbw = v[2] + local vhtbw = diff["VHT_BW"] and tostring(diff["VHT_BW"][2]) or tostring(cfgs["VHT_BW"]) + for i=1, #vifidx do + commands_vifs = string.format([[ + iwpriv %s set HtBw=%s;]], vifext..tostring(tonumber(i)-1), tostring(v[2])) + commands_1 = string.format([[ + iwpriv %s set VhtBw=%s;]], vifext..tostring(tonumber(i)-1), tostring(vhtbw)) + -- workaround + if htbw == "1" and vhtbw == "0" then + commands_2 = string.format([[ + iwpriv %s set HtBssCoex=%s;]], vifext..tostring(tonumber(i)-1), diff["HT_BSSCoexistence"] + and tostring(diff["HT_BSSCoexistence"][2]) or tostring(cfgs["HT_BSSCoexistence"])) + else + commands_2 = string.format([[ + iwpriv %s set HtBssCoex=0;]], vifext..tostring(tonumber(i)-1)) + end + if commands_vifs then + debug_info_write(devname, commands_vifs) + debug_info_write(devname, commands_1) + debug_info_write(devname, commands_2) + end + end + commands_vifs_ssid = true + commands_bw = true + elseif not commands_bw and k == "VHT_BW" then + local vhtbw = v[2] + local htbw = diff["HT_BW"] and tostring(diff["HT_BW"][2]) or tostring(cfgs["HT_BW"]) + for i=1, #vifidx do + commands_vifs = string.format([[ + iwpriv %s set VhtBw=%s;]], vifext..tostring(tonumber(i)-1), tostring(v[2])) + commands_1 = string.format([[ + iwpriv %s set HtBw=%s;]], vifext..tostring(tonumber(i)-1), tostring(htbw) + or tostring(cfgs["HT_BW"])) + -- workaround + if htbw == "1" and vhtbw == "0" then + commands_2 = string.format([[ + iwpriv %s set HtBssCoex=%s;]], vifext..tostring(tonumber(i)-1), diff["HT_BSSCoexistence"] + and tostring(diff["HT_BSSCoexistence"][2]) or tostring(cfgs["HT_BSSCoexistence"])) + else + commands_2 = string.format([[ + iwpriv %s set HtBssCoex=0;]], vifext..tostring(tonumber(i)-1)) + end + if commands_vifs then + debug_info_write(devname, commands_vifs) + debug_info_write(devname, commands_1) + debug_info_write(devname, commands_2) + end + end + commands_vifs_ssid = true + commands_bw = true + elseif k:find("AccessPolicy") then + local index = string.match(k, '%d') + if commands_access_2[index] then return end + + commands_vifs = string.format([[ + iwpriv %s set AccessPolicy=%s;]], vifext..tostring(index), tostring(v[2])) + debug_info_write(devname, commands_vifs) + if v[2] == '0' then break end + + -- Delete all entry first + local commands_del_list = string.format([[ + iwpriv %s set ACLClearAll=1;]], vifext..tostring(index)) + debug_info_write(devname, commands_del_list) + local list_old = cfgs["AccessControlList"..tostring(index)]:split() or {} + local list_old_i = (list_old[1] or ''):split(";") + local list_new = diff["AccessControlList"..tostring(index)] and diff["AccessControlList"..tostring(index)][2]:split() or {} + local list_old_i = (list_old[1] or ''):split(";") + local list_new_i = (list_new[1] or ''):split(";") + + if diff["AccessControlList"..tostring(index)] then + for i=1, #list_new_i do + local commands_aclist = string.format([[ + iwpriv %s set ACLAddEntry=%s;]], vifext..tostring(index), list_new_i[i]) + debug_info_write(devname, commands_aclist) + end + elseif cfgs["AccessControlList"..tostring(index)] and cfgs["AccessControlList"..tostring(index)] ~= "" then + for i=1, #list_old_i do + local commands_aclist = string.format([[ + iwpriv %s set ACLAddEntry=%s;]], vifext..tostring(index), list_old_i[i]) + debug_info_write(devname, commands_aclist) + end + end + commands_access_1[index] = true + elseif k:find("AccessControlList") then + local index = string.match(k, '%d') + if commands_access_1[index] then return end + -- Clear all entry first + local commands_del_list = string.format([[ + iwpriv %s set ACLClearAll=1;]], vifext..tostring(index)) + debug_info_write(devname, commands_del_list) + -- Then add entries + local commands_ac = string.format([[ + iwpriv %s set AccessPolicy=%s;]], vifext..tostring(index), diff["AccessPolicy"..tostring(index)] + and tostring(diff["AccessPolicy"..tostring(index)][2]) or cfgs["AccessPolicy"..tostring(index)]) + debug_info_write(devname, commands_ac) + local list_new = diff["AccessControlList"..tostring(index)] and diff["AccessControlList"..tostring(index)][2]:split() or {} + local list_new_i = (list_new[1] or ''):split(";") + + if diff["AccessControlList"..tostring(index)] and #list_new_i > 0 then + for i=1, #list_new_i do + local commands_aclist = string.format([[ + iwpriv %s set ACLAddEntry=%s;]], vifext..tostring(index), list_new_i[i]) + debug_info_write(devname, commands_aclist) + end + end + commands_access_2[index] = true + + -- Now I assume that the reset keywords are we did not consider, + -- and they all match the dat's keywords + -- So, I simply set the "iwpriv vif set k=v" + elseif vifs_cfg_parm(k) and vifs_cfg_parm(v[2]) then + if string.find(k, "ApCli") then + commands = string.format([[ + iwpriv %s set %s=%s;]], vifapcli..tostring(0), k, tostring(v[2])) + else + commands = string.format([[ + iwpriv %s set %s=%s;]], vifname, k, tostring(v[2])) + end + end + + if commands then + commands_dev = true -- it will set iwpriv vif set SSID=xxx; + debug_info_write(devname, commands) + elseif commandns then + debug_info_write(devname, commandns) + end + end + + if commands_vifs_ssid then + for i=1, #vifidx do + commands_vifs_ssid = string.format([[ + iwpriv %s set SSID="%s";]], vifext..tostring(tonumber(i)-1), diff["SSID"..tostring(i)] and + tostring(diff["SSID"..tostring(i)][2]) or cfgs["SSID"..tostring(i)]) + debug_info_write(devname, commands_vifs_ssid) + end + else + local ssid1 = true + for i=1, #vifidx do + if commands_ssid[i] then + debug_info_write(devname, commands_ssid[i]) + if i == 1 then ssid1 = false end + end + end + if ssid1 and commands_dev then debug_info_write(devname, last_command) end + end +end + +function quick_settings(devname,path) + local mtkwifi = require("mtkwifi") + local devs, l1parser = mtkwifi.__get_l1dat() + local path_last, cfgs, diff, device + assert(l1parser, "failed to parse l1profile!") + + -- If there is't /tmp/mtk/wifi/devname.last, wifi down/wifi up is necessary. + -- Case 1: The first time wifi setup; + -- Case 2: When reload wifi by pressing UI button. + if not mtkwifi.exists("/tmp/mtk/wifi/"..string.match(path, "([^/]+)\.dat")..".last") then + need_downup = true + end + -- Copy /tmp/mtk/wifi/devname.applied to /tmp/mtk/wifi/devname.last for diff + if not mtkwifi.exists("/tmp/mtk/wifi/"..string.match(path, "([^/]+)\.dat")..".applied") then + os.execute("cp -f "..path.." "..mtkwifi.__profile_previous_settings_path(path)) + else + os.execute("cp -f "..mtkwifi.__profile_applied_settings_path(path).. + " "..mtkwifi.__profile_previous_settings_path(path)) + end + -- there are no /tmp/mtk/wifi/devname.last + if need_downup then return true end + + path_last = mtkwifi.__profile_previous_settings_path(path) + diff = mtkwifi.diff_profile(path_last, path) + cfgs = mtkwifi.load_profile(path_last) + + if not next(diff) then return true end -- diff == nil + + -- It maybe better to save this parms in a new file. + need_downup_parms = {"HT_LDPC", "VHT_SGI", "VHT_LDPC", "idle_timeout_interval", + "E2pAccessMode", "MUTxRxEnable","DLSCapable","VHT_Sec80_Channel", + "Wds", "PowerUpenable", "session_timeout_interval", "MapMode", + "ChannelGrp","TxOP"} + + for k, v in pairs(diff) do + nixio.syslog("debug", "quick_settings diff : "..k.."="..v[2]) + for _, pat in ipairs(need_downup_parms) do + if string.find(k, pat) then + need_downup = true; + nixio.syslog("debug", "quick_settings: need_downup "..k.."="..v[2]) + break + end + end + if need_downup then break end + end + if need_downup then return true end + + + -- Quick Setting + os.execute("rm -rf /tmp/mtk/wifi/"..devname.."_quick_setting_cmd.sh") + device = devs.devname_ridx[devname] + + -- need to set Authmode and Encry before MFPC&MFPR + if cfgs["ApCliEnable"] and cfgs["ApCliEnable"] == "1" or + diff["ApCliEnable"] and diff["ApCliEnable"][2] =="1" then + __set_wifi_apcli_security(cfgs, diff, device, devname) + end + + __set_wifi_misc(cfgs, diff, device, devname) + + __set_he_mu(cfgs, diff, device, devname) + + -- security is complicated enough to get a special API + __set_wifi_security(cfgs, diff, device, devname) + + --execute all iwpriv cmd + os.execute("sh /tmp/mtk/wifi/"..devname.."_quick_setting_cmd.sh") + + -- save the quick seting log, we assume it can hold up to 10000 at most. + if mtkwifi.exists("/tmp/mtk/wifi/"..devname.."_quick_setting_cmd.sh") then + if mtkwifi.exists("/tmp/mtk/wifi/quick_setting_cmds.log") then + filesize = GetFileSize("/tmp/mtk/wifi/quick_setting_cmds.log") + if filesize > 10000 then + os.execute("mv -f /tmp/mtk/wifi/quick_setting_cmds.log /tmp/mtk/wifi/quick_setting_cmds_bak.log") + end + end + os.execute("echo ............................................... >> /tmp/mtk/wifi/quick_setting_cmds.log") + os.execute("cat /tmp/mtk/wifi/"..devname.."_quick_setting_cmd.sh >> /tmp/mtk/wifi/quick_setting_cmds.log") + end + return false +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.luac b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.luac new file mode 100644 index 000000000000..6419c9715a47 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.lua b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.lua new file mode 100755 index 000000000000..a6feaf8eaaf6 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.lua @@ -0,0 +1,189 @@ +--This file is created for check some deamons + + local mtkwifi = require("mtkwifi") + local devs = mtkwifi.get_all_devs() + local nixio = require("nixio") + +function miniupnpd_chk(devname,vif,enable) + local WAN_IF=mtkwifi.__trim(mtkwifi.read_pipe("uci -q get network.wan.ifname")) + + os.execute("rm -rf /etc/miniupnpd.conf") + + if mtkwifi.exists("/tmp/run/miniupnpd."..vif) then + os.execute("cat /tmp/run/miniupnpd."..vif.." | xargs kill -9") + end + + if enable then + local profile = mtkwifi.search_dev_and_profile()[devname] + local cfgs = mtkwifi.load_profile(profile) + local ssid_index = devs[devname]["vifs"][vif].vifidx + local wsc_conf_mode = "" + local PORT_NUM = 7777+(string.byte(vif, -1)+string.byte(vif, -2)) + local LAN_IPADDR = mtkwifi.__trim(mtkwifi.read_pipe("uci -q get network.lan.ipaddr")) + local LAN_MASK = mtkwifi.__trim(mtkwifi.read_pipe("uci -q get network.lan.netmask")) + local port = 6352 + (string.byte(vif, -1)+string.byte(vif, -2)) + LAN_IPADDR = LAN_IPADDR.."/"..LAN_MASK + wsc_conf_mode = mtkwifi.token_get(cfgs["WscConfMode"], ssid_index, "") + + local file = io.open("/etc/miniupnpd.conf", "w") + if nil == file then + nixio.syslog("debug","open file /etc/miniupnpd.conf fail") + end + + file:write("ext_ifname=",WAN_IF,'\n','\n', + "listening_ip=",LAN_IPADDR,'\n','\n', + "port=",port,'\n','\n', + "bitrate_up=800000000",'\n', + "bitrate_down=800000000",'\n','\n', + "secure_mode=no",'\n','\n', + "system_uptime=yes",'\n','\n', + "notify_interval=30",'\n','\n', + "uuid=68555350-3352-3883-2883-335030522880",'\n','\n', + "serial=12345678",'\n','\n', + "model_number=1",'\n','\n', + "enable_upnp=no",'\n','\n') + file:close() + + if wsc_conf_mode ~= "" and wsc_conf_mode ~= "0" then + os.execute("miniupnpd -m 1 -I "..vif.." -P /var/run/miniupnpd."..vif.." -G -i "..WAN_IF.." -a "..LAN_IPADDR.." -n "..PORT_NUM) + end + end +end + +function d8021xd_chk(devname, prefix, vif, enable) + if mtkwifi.exists("/tmp/run/8021xd_"..vif..".pid") then + os.execute("cat /tmp/run/8021xd_"..vif..".pid | xargs kill -9") + os.execute("rm /tmp/run/8021xd_"..vif..".pid") + end + + if enable then + local profile = mtkwifi.search_dev_and_profile()[devname] + local cfgs = mtkwifi.load_profile(profile) + local auth_mode = cfgs.AuthMode + local ieee8021x = cfgs.IEEE8021X + local pat_auth_mode = {"WPA$", "WPA;", "WPA2$", "WPA2;", "WPA1WPA2$", "WPA1WPA2;", "WPA3$", "WPA3;", "192$", "192;", "WPA2-Ent-OSEN$", "WPA2-Ent-OSEN;"} + local pat_ieee8021x = {"1$", "1;"} + local apd_en = false + + for _, pat in ipairs(pat_auth_mode) do + if string.find(auth_mode, pat) then + apd_en = true + end + end + + for _, pat in ipairs(pat_ieee8021x) do + if string.find(ieee8021x, pat) then + apd_en = true + end + end + + if apd_en then + os.execute("8021xd -p "..prefix.. " -i "..vif) + end + end +end + +local function get_viflist() + local devs = mtkwifi.get_all_devs() + local vif_list = {} + for _,dev in ipairs(devs) do + for _,vif in ipairs(dev.vifs) do + if not(string.find(dev.profile, "ax7800") and vif.vifname == "ra0") then + table.insert(vif_list,vif.vifname) + end + end + end + return vif_list +end + +-- wifi service that require to start after wifi up +function wifi_service_misc() + local mapd_default = mtkwifi.load_profile("/etc/map/mapd_default.cfg") + local mapd_user = mtkwifi.load_profile("/etc/map/mapd_user.cfg") + local first_card_cfgs = mtkwifi.load_profile(mtkwifi.detect_first_card()) + + local vif_list = get_viflist() + local total_vif = #vif_list + local over_time = 0 + while over_time < 30 do + up_num = 0 + for _,vif in pairs(vif_list) do + local is_up = string.find(mtkwifi.__trim(mtkwifi.read_pipe("ifconfig "..vif.." | grep UP")), "UP") ~= nil + if is_up then + up_num = up_num+1 + end + end + if up_num == total_vif then + break + else + over_time = over_time + 1 + os.execute("sleep 1") + end + end + + if mapd_default.mode then + local eth_mode = mapd_default.mode + local device_role = mapd_default.DeviceRole + if mapd_user.mode then + eth_mode = mapd_user.mode + end + if mapd_user.DeviceRole then + device_role = mapd_user.DeviceRole + end + -- Start Hostapd if exists + if ((first_card_cfgs.MapMode == "1") and mtkwifi.exists("/usr/bin/hostapd")) then + os.execute("killall hostapd") + local hostapd_cmd = "" + for _,ifname in ipairs(string.split(mtkwifi.read_pipe("ls /sys/class/net"), "\n")) + do + if (string.match(ifname,"ra")) then + hostapd_cmd = hostapd_cmd.." /etc/hostapd_"..ifname.."_map.conf" + end + end + print(hostapd_cmd) + os.execute("echo "..hostapd_cmd.." >/dev/console") + os.execute("/usr/bin/hostapd -B "..hostapd_cmd) + end + -- 1.Wapp + if mtkwifi.exists("/usr/bin/wapp_openwrt.sh") then + os.execute("./etc/init.d/wapp start") + end + -- 2.EasyMesh + if mtkwifi.exists("/usr/bin/EasyMesh_openwrt.sh") then + if first_card_cfgs.MapMode == "1" then + if (eth_mode == "0" and device_role == "1") or eth_mode == "1" then + os.execute("./etc/init.d/easymesh start") + else + os.execute("./etc/init.d/easymesh_bridge start") + end + else + os.execute("./etc/init.d/easymesh start") + end + end + else + -- 1.Wapp + if mtkwifi.exists("/usr/bin/wapp_openwrt.sh") then + os.execute("./etc/init.d/wapp start") + end + -- 2.EasyMesh + if mtkwifi.exists("/usr/bin/EasyMesh_openwrt.sh") then + os.execute("./etc/init.d/easymesh start") + end + end + -- Start AFC + os.execute("killall AFC") + if mtkwifi.exists("/usr/bin/AFC") then + local afc_cmd = "rax0 &" + os.execute("echo start AFC in background > /dev/console") + os.execute("/usr/bin/AFC "..afc_cmd) + end +end + +-- wifi service that require to clean up before wifi down +function wifi_service_misc_clean() + os.execute("rm -rf /tmp/wapp_ctrl") + os.execute("killall -15 mapd") + os.execute("killall -15 wapp") + os.execute("killall -15 p1905_managerd") + os.execute("killall -15 bs20") +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.luac b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.luac new file mode 100644 index 000000000000..a27d76fca433 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.lua b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.lua new file mode 100755 index 000000000000..985fe2c9605e --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.lua @@ -0,0 +1,91 @@ +#!/usr/bin/env lua +--This file is created for check some deamons like miniupnpd,8021xd... + +function __trim(s) + if s then return (s:gsub("^%s*(.-)%s*$", "%1")) end +end + +function exists(path) + local fp = io.open(path, "rb") + if fp then fp:close() end + return fp ~= nil +end + +function load_profile(path) + local cfgs = {} + local fd = io.open(path, "r") + if not fd then return cfgs end + + for line in fd:lines() do + line = __trim(line) + if string.byte(line) ~= string.byte("#") then + local i = string.find(line, "=") + if i then + local k,v + k = string.sub(line, 1, i-1) + v = string.sub(line, i+1) + -- if cfgs[__trim(k)] then + -- print("warning", "skip repeated key"..line) + -- end + cfgs[__trim(k)] = __trim(v) or "" + -- else + -- print("warning", "skip line without '=' "..line) + end + -- else + -- print("warning", "skip comment line "..line) + end + end + fd:close() + return cfgs +end + +function d8021xd_chk(profile,prefix,main_ifname,enable) + local cfgs = load_profile(profile) + local auth_mode = cfgs.AuthMode + local ieee8021x = cfgs.IEEE8021X + local pat_auth_mode = {"WPA$", "WPA;", "WPA2$", "WPA2;", "WPA1WPA2$", "WPA1WPA2;"} + local pat_ieee8021x = {"1$", "1;"} + local apd_en = false + if exists("/tmp/run/8021xd_"..main_ifname..".pid") then + os.execute("cat /tmp/run/8021xd_"..main_ifname..".pid | xargs kill -9") + os.execute("rm /tmp/run/8021xd_"..main_ifname..".pid") + end + if enable == "true" then + for _, pat in ipairs(pat_auth_mode) do + if string.find(auth_mode, pat) then + apd_en = true + end + end + + for _, pat in ipairs(pat_ieee8021x) do + if string.find(ieee8021x, pat) then + apd_en = true + end + end + + if apd_en then + os.execute("8021xd -p "..prefix.. " -i "..main_ifname) + end + end +end + +-- wifi service that require to start after wifi up +function wifi_service_misc() + -- 1.Wapp + if exists("/usr/bin/wapp_openwrt.sh") then + os.execute("/usr/bin/wapp_openwrt.sh") + end + + -- 2.EasyMesh + if exists("/usr/bin/EasyMesh_openwrt.sh") then + os.execute("/usr/bin/EasyMesh_openwrt.sh") + end +end + +service = arg[1] + +if service == "s8021x" then + d8021xd_chk(arg[2], arg[3], arg[4], arg[5]) +elseif service == "wifi_service_misc" then + wifi_service_misc() +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.luac b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.luac new file mode 100644 index 000000000000..fa8c5fdb97bc Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.lua new file mode 100755 index 000000000000..4763028cb279 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.lua @@ -0,0 +1,349 @@ +#!/usr/bin/env lua + +--[[ + * A lua library to manipulate mtk's wifi driver. used in luci-app-mtk. + * + * Copyright (C) 2016 MTK + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] + +local l1dat_parser = { + L1_DAT_PATH = "/etc/wireless/l1profile.dat", + IF_RINDEX = "ifname_ridx", + DEV_RINDEX = "devname_ridx", + MAX_NUM_APCLI = 1, + MAX_NUM_WDS = 4, + MAX_NUM_MESH = 1, + MAX_NUM_EXTIF = 16, + MAX_NUM_DBDC_BAND = 2, +} + +local l1cfg_options = { + ext_ifname="", + apcli_ifname="apcli", + wds_ifname="wds", + mesh_ifname="mesh" + } + +function l1dat_parser.__trim(s) + if s then return (s:gsub("^%s*(.-)%s*$", "%1")) end +end + +function l1dat_parser.__cfg2list(str) + -- delimeter == ";" + local i = 1 + local list = {} + for k in string.gmatch(str, "([^;]+)") do + list[i] = k + i = i + 1 + end + return list +end + +function l1dat_parser.token_get(str, n, v) + -- n starts from 1 + -- v is the backup in case token n is nil + if not str then return v end + local tmp = l1dat_parser.__cfg2list(str) + return tmp[tonumber(n)] or v +end + +function l1dat_parser.add_default_value(l1cfg) + for k, v in ipairs(l1cfg) do + + for opt, default in pairs(l1cfg_options) do + if ( opt == "ext_ifname" ) then + v[opt] = v[opt] or v["main_ifname"].."_" + else + v[opt] = v[opt] or default..k.."_" + end + end + end + + return l1cfg +end + +function l1dat_parser.get_value_by_idx(devidx, mainidx, subidx, key) + --print("Enter l1dat_parser.get_value_by_idx("..devidx..","..mainidx..", "..subidx..", "..key..")
    ") + if not devidx or not mainidx or not key then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local dev_ridx = l1dat_parser.DEV_RINDEX + local sidx = subidx or 1 + local devname1 = devidx.."."..mainidx + local devname2 = devidx.."."..mainidx.."."..sidx + + --print("devnam1=", devname1, "devname2=", devname2, "
    ") + return devs[dev_ridx][devname2] and devs[dev_ridx][devname2][key] + or devs[dev_ridx][devname1] and devs[dev_ridx][devname1][key] +end + +-- path to zone is 1 to 1 mapping +function l1dat_parser.l1_path_to_zone(path) + --print("Enter l1dat_parser.l1_path_to_zone("..path..")
    ") + if not path then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + for _, dev in pairs(devs[l1dat_parser.IF_RINDEX]) do + if dev.profile_path == path then + return dev.nvram_zone + end + end + + return +end + +-- zone to path is 1 to n mapping +function l1dat_parser.l1_zone_to_path(zone) + if not zone then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local plist = {} + for _, dev in pairs(devs[l1dat_parser.IF_RINDEX]) do + if dev.nvram_zone == zone then + if not next(plist) then + table.insert(plist,dev.profile_path) + else + local plist_str = table.concat(plist) + if not plist_str:match(dev.profile_path) then + table.insert(plist,dev.profile_path) + end + end + end + end + + return next(plist) and plist or nil +end + +function l1dat_parser.l1_ifname_to_datpath(ifname) + if not ifname then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local ridx = l1dat_parser.IF_RINDEX + return devs[ridx][ifname] and devs[ridx][ifname].profile_path +end + +function l1dat_parser.l1_ifname_to_zone(ifname) + if not ifname then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local ridx = l1dat_parser.IF_RINDEX + return devs[ridx][ifname] and devs[ridx][ifname].nvram_zone +end + +function l1dat_parser.l1_zone_to_ifname(zone) + if not zone then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local zone_dev + for _, dev in pairs(devs[l1dat_parser.DEV_RINDEX]) do + if dev.nvram_zone == zone then + zone_dev = dev + end + end + + if not zone_dev then + return nil + else + return zone_dev.main_ifname, zone_dev.ext_ifname, zone_dev.apcli_ifname, zone_dev.wds_ifname, zone_dev.mesh_ifname + end +end + +-- input: L1 profile path. +-- output A table, devs, contains +-- 1. devs[%d] = table of each INDEX# in the L1 profile +-- 2. devs.ifname_ridx[ifname] +-- = table of each ifname and point to relevant contain in dev[$d] +-- 3. devs.devname_ridx[devname] similar to devs.ifnameridx, but use devname. +-- devname = INDEX#_value.mainidx(.subidx) +-- Using *_ridx do not need to handle name=k1;k2 case of DBDC card. +function l1dat_parser.load_l1_profile(path) + local devs = setmetatable({}, {__index= + function(tbl, key) + local util = require("luci.util") + --print("metatable function:", util.serialize_data(tbl), key) + --print("-----------------------------------------------") + if ( string.match(key, "^%d+")) then + tbl[key] = {} + return tbl[key] + end + end + }) + local nixio = require("nixio") + local chipset_num = {} + local dir = io.popen("ls /etc/wireless/") + if not dir then return end + local fd = io.open(path, "r") + if not fd then return end + + -- convert l1 profile into lua table + for line in fd:lines() do + line = l1dat_parser.__trim(line) + if string.byte(line) ~= string.byte("#") then + local i = string.find(line, "=") + if i then + local k, v, k1, k2 + k = l1dat_parser.__trim( string.sub(line, 1, i-1) ) + v = l1dat_parser.__trim( string.sub(line, i+1) ) + k1, k2 = string.match(k, "INDEX(%d+)_(.+)") + if k1 then + k1 = tonumber(k1) + 1 + if devs[k1][k2] then + nixio.syslog("warning", "skip repeated key"..line) + end + devs[k1][k2] = v or "" + else + k1 = string.match(k, "INDEX(%d+)") + k1 = tonumber(k1) + 1 + devs[k1]["INDEX"] = v + + chipset_num[v] = (not chipset_num[v] and 1) or chipset_num[v] + 1 + devs[k1]["mainidx"] = chipset_num[v] + end + else + nixio.syslog("warning", "skip line without '=' "..line) + end + else + nixio.syslog("warning", "skip comment line "..line) + end + end + + l1dat_parser.add_default_value(devs) + --local util = require("luci.util") + --local seen2 = {} + -- print("Before setup ridx", util.serialize_data(devs, seen2)) + + -- Force to setup reverse indice for quick search. + -- Benifit: + -- 1. O(1) search with ifname, devname + -- 2. Seperate DBDC name=k1;k2 format in the L1 profile into each + -- ifname, devname. + local dbdc_if = {} + local ridx = l1dat_parser.IF_RINDEX + local dridx = l1dat_parser.DEV_RINDEX + local band_num = l1dat_parser.MAX_NUM_DBDC_BAND + local k, v, dev, i , j, last + local devname + devs[ridx] = {} + devs[dridx] = {} + for _, dev in ipairs(devs) do + dbdc_if[band_num] = l1dat_parser.token_get(dev.main_ifname, band_num, nil) + if dbdc_if[band_num] then + for i = 1, band_num - 1 do + dbdc_if[i] = l1dat_parser.token_get(dev.main_ifname, i, nil) + end + for i = 1, band_num do + devs[ridx][dbdc_if[i]] = {} + devs[ridx][dbdc_if[i]]["subidx"] = i + + for k, v in pairs(dev) do + if k == "INDEX" or k == "EEPROM_offset" or k == "EEPROM_size" + or k == "mainidx" then + devs[ridx][dbdc_if[i]][k] = v + else + devs[ridx][dbdc_if[i]][k] = l1dat_parser.token_get(v, i, "") + end + end + devname = dev.INDEX.."."..dev.mainidx.."."..devs[ridx][dbdc_if[i]]["subidx"] + devs[dridx][devname] = devs[ridx][dbdc_if[i]] + end + + local apcli_if, wds_if, ext_if, mesh_if = {}, {}, {}, {} + + for i = 1, band_num do + ext_if[i] = l1dat_parser.token_get(dev.ext_ifname, i, nil) + apcli_if[i] = l1dat_parser.token_get(dev.apcli_ifname, i, nil) + wds_if[i] = l1dat_parser.token_get(dev.wds_ifname, i, nil) + mesh_if[i] = l1dat_parser.token_get(dev.mesh_ifname, i, nil) + end + + for i = 1, l1dat_parser.MAX_NUM_EXTIF - 1 do -- ifname idx is from 0 + for j = 1, band_num do + devs[ridx][ext_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + + for i = 0, l1dat_parser.MAX_NUM_APCLI - 1 do + for j = 1, band_num do + devs[ridx][apcli_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + + for i = 0, l1dat_parser.MAX_NUM_WDS - 1 do + for j = 1, band_num do + devs[ridx][wds_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + + for i = 0, l1dat_parser.MAX_NUM_MESH - 1 do + for j = 1, band_num do + if mesh_if[j] then + devs[ridx][mesh_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + end + + else + devs[ridx][dev.main_ifname] = dev + + devname = dev.INDEX.."."..dev.mainidx + devs[dridx][devname] = dev + + for i = 1, l1dat_parser.MAX_NUM_EXTIF - 1 do -- ifname idx is from 0 + devs[ridx][dev.ext_ifname..i] = dev + end + + for i = 0, l1dat_parser.MAX_NUM_APCLI - 1 do -- ifname idx is from 0 + devs[ridx][dev.apcli_ifname..i] = dev + end + + for i = 0, l1dat_parser.MAX_NUM_WDS - 1 do -- ifname idx is from 0 + devs[ridx][dev.wds_ifname..i] = dev + end + + for i = 0, l1dat_parser.MAX_NUM_MESH - 1 do -- ifname idx is from 0 + devs[ridx][dev.mesh_ifname..i] = dev + end + end + end + + fd:close() + return devs +end + +function l1dat_parser.creat_link_for_nvram( ) + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + for devname, dev in pairs(devs.devname_ridx) do + local dev = devs.devname_ridx[devname] + profile = dev.profile_path + os.execute("mkdir -p /tmp/mtk/wifi/") + if dev.nvram_zone == "dev1" then + os.execute("ln -sf " ..profile.." /tmp/mtk/wifi/2860") + elseif dev.nvram_zone == "dev2" then + os.execute("ln -sf " ..profile.." /tmp/mtk/wifi/rtdev") + elseif dev.nvram_zone == "dev3" then + os.execute("ln -sf " ..profile.." /tmp/mtk/wifi/wifi3") + end + end +end +return l1dat_parser diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.luac new file mode 100644 index 000000000000..880a6484c872 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.lua new file mode 100644 index 000000000000..afa735dc2cec --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.lua @@ -0,0 +1,319 @@ +----------------------------------------------------------------------------- +-- LTN12 - Filters, sources, sinks and pumps. +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local table = require("table") +local unpack = unpack or table.unpack +local base = _G +local _M = {} +if module then -- heuristic for exporting a global package table + ltn12 = _M +end +local filter,source,sink,pump = {},{},{},{} + +_M.filter = filter +_M.source = source +_M.sink = sink +_M.pump = pump + +local unpack = unpack or table.unpack +local select = base.select + +-- 2048 seems to be better in windows... +_M.BLOCKSIZE = 2048 +_M._VERSION = "LTN12 1.0.3" + +----------------------------------------------------------------------------- +-- Filter stuff +----------------------------------------------------------------------------- +-- returns a high level filter that cycles a low-level filter +function filter.cycle(low, ctx, extra) + base.assert(low) + return function(chunk) + local ret + ret, ctx = low(ctx, chunk, extra) + return ret + end +end + +-- chains a bunch of filters together +-- (thanks to Wim Couwenberg) +function filter.chain(...) + local arg = {...} + local n = base.select('#',...) + local top, index = 1, 1 + local retry = "" + return function(chunk) + retry = chunk and retry + while true do + if index == top then + chunk = arg[index](chunk) + if chunk == "" or top == n then return chunk + elseif chunk then index = index + 1 + else + top = top+1 + index = top + end + else + chunk = arg[index](chunk or "") + if chunk == "" then + index = index - 1 + chunk = retry + elseif chunk then + if index == n then return chunk + else index = index + 1 end + else base.error("filter returned inappropriate nil") end + end + end + end +end + +----------------------------------------------------------------------------- +-- Source stuff +----------------------------------------------------------------------------- +-- create an empty source +local function empty() + return nil +end + +function source.empty() + return empty +end + +-- returns a source that just outputs an error +function source.error(err) + return function() + return nil, err + end +end + +-- creates a file source +function source.file(handle, io_err) + if handle then + return function() + local chunk = handle:read(_M.BLOCKSIZE) + if not chunk then handle:close() end + return chunk + end + else return source.error(io_err or "unable to open file") end +end + +-- turns a fancy source into a simple source +function source.simplify(src) + base.assert(src) + return function() + local chunk, err_or_new = src() + src = err_or_new or src + if not chunk then return nil, err_or_new + else return chunk end + end +end + +-- creates string source +function source.string(s) + if s then + local i = 1 + return function() + local chunk = string.sub(s, i, i+_M.BLOCKSIZE-1) + i = i + _M.BLOCKSIZE + if chunk ~= "" then return chunk + else return nil end + end + else return source.empty() end +end + +-- creates table source +function source.table(t) + base.assert('table' == type(t)) + local i = 0 + return function() + i = i + 1 + return t[i] + end +end + +-- creates rewindable source +function source.rewind(src) + base.assert(src) + local t = {} + return function(chunk) + if not chunk then + chunk = table.remove(t) + if not chunk then return src() + else return chunk end + else + table.insert(t, chunk) + end + end +end + +-- chains a source with one or several filter(s) +function source.chain(src, f, ...) + if ... then f=filter.chain(f, ...) end + base.assert(src and f) + local last_in, last_out = "", "" + local state = "feeding" + local err + return function() + if not last_out then + base.error('source is empty!', 2) + end + while true do + if state == "feeding" then + last_in, err = src() + if err then return nil, err end + last_out = f(last_in) + if not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + elseif last_out ~= "" then + state = "eating" + if last_in then last_in = "" end + return last_out + end + else + last_out = f(last_in) + if last_out == "" then + if last_in == "" then + state = "feeding" + else + base.error('filter returned ""') + end + elseif not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + else + return last_out + end + end + end + end +end + +-- creates a source that produces contents of several sources, one after the +-- other, as if they were concatenated +-- (thanks to Wim Couwenberg) +function source.cat(...) + local arg = {...} + local src = table.remove(arg, 1) + return function() + while src do + local chunk, err = src() + if chunk then return chunk end + if err then return nil, err end + src = table.remove(arg, 1) + end + end +end + +----------------------------------------------------------------------------- +-- Sink stuff +----------------------------------------------------------------------------- +-- creates a sink that stores into a table +function sink.table(t) + t = t or {} + local f = function(chunk, err) + if chunk then table.insert(t, chunk) end + return 1 + end + return f, t +end + +-- turns a fancy sink into a simple sink +function sink.simplify(snk) + base.assert(snk) + return function(chunk, err) + local ret, err_or_new = snk(chunk, err) + if not ret then return nil, err_or_new end + snk = err_or_new or snk + return 1 + end +end + +-- creates a file sink +function sink.file(handle, io_err) + if handle then + return function(chunk, err) + if not chunk then + handle:close() + return 1 + else return handle:write(chunk) end + end + else return sink.error(io_err or "unable to open file") end +end + +-- creates a sink that discards data +local function null() + return 1 +end + +function sink.null() + return null +end + +-- creates a sink that just returns an error +function sink.error(err) + return function() + return nil, err + end +end + +-- chains a sink with one or several filter(s) +function sink.chain(f, snk, ...) + if ... then + local args = { f, snk, ... } + snk = table.remove(args, #args) + f = filter.chain(unpack(args)) + end + base.assert(f and snk) + return function(chunk, err) + if chunk ~= "" then + local filtered = f(chunk) + local done = chunk and "" + while true do + local ret, snkerr = snk(filtered, err) + if not ret then return nil, snkerr end + if filtered == done then return 1 end + filtered = f(done) + end + else return 1 end + end +end + +----------------------------------------------------------------------------- +-- Pump stuff +----------------------------------------------------------------------------- +-- pumps one chunk from the source to the sink +function pump.step(src, snk) + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + if chunk and ret then return 1 + else return nil, src_err or snk_err end +end + +-- pumps all data from a source to a sink, using a step function +function pump.all(src, snk, step) + base.assert(src and snk) + step = step or pump.step + while true do + local ret, err = step(src, snk) + if not ret then + if err then return nil, err + else return 1 end + end + end +end + +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.luac new file mode 100644 index 000000000000..65b12a42f9b3 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.lua new file mode 100644 index 000000000000..7ef971df8dae --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.lua @@ -0,0 +1,12 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local config = require "luci.config" +local ccache = require "luci.ccache" + +module "luci.cacheloader" + +if config.ccache and config.ccache.enable == "1" then + ccache.cache_ondemand() +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.luac new file mode 100644 index 000000000000..7ce0e7b105c2 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.lua new file mode 100644 index 000000000000..d3be7cba6c64 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.lua @@ -0,0 +1,76 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local io = require "io" +local fs = require "nixio.fs" +local util = require "luci.util" +local nixio = require "nixio" +local debug = require "debug" +local string = require "string" +local package = require "package" + +local type, loadfile = type, loadfile + + +module "luci.ccache" + +function cache_ondemand(...) + if debug.getinfo(1, 'S').source ~= "=?" then + cache_enable(...) + end +end + +function cache_enable(cachepath, mode) + cachepath = cachepath or "/tmp/luci-modulecache" + mode = mode or "r--r--r--" + + local loader = package.loaders[2] + local uid = nixio.getuid() + + if not fs.stat(cachepath) then + fs.mkdir(cachepath) + end + + local function _encode_filename(name) + local encoded = "" + for i=1, #name do + encoded = encoded .. ("%2X" % string.byte(name, i)) + end + return encoded + end + + local function _load_sane(file) + local stat = fs.stat(file) + if stat and stat.uid == uid and stat.modestr == mode then + return loadfile(file) + end + end + + local function _write_sane(file, func) + if nixio.getuid() == uid then + local fp = io.open(file, "w") + if fp then + fp:write(util.get_bytecode(func)) + fp:close() + fs.chmod(file, mode) + end + end + end + + package.loaders[2] = function(mod) + local encoded = cachepath .. "/" .. _encode_filename(mod) + local modcons = _load_sane(encoded) + + if modcons then + return modcons + end + + -- No cachefile + modcons = loader(mod) + if type(modcons) == "function" then + _write_sane(encoded, modcons) + end + return modcons + end +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.luac new file mode 100644 index 000000000000..48fd665fcf43 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.lua new file mode 100644 index 000000000000..d01153f4f564 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.lua @@ -0,0 +1,18 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +module("luci.config", + function(m) + if pcall(require, "luci.model.uci") then + local config = util.threadlocal() + setmetatable(m, { + __index = function(tbl, key) + if not config[key] then + config[key] = luci.model.uci.cursor():get_all("luci", key) + end + return config[key] + end + }) + end + end) diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.luac new file mode 100644 index 000000000000..3be66cf0c45f Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.lua new file mode 100644 index 000000000000..736d0cdccff3 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.lua @@ -0,0 +1,196 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +module("luci.controller.admin.index", package.seeall) + +function action_logout() + local dsp = require "luci.dispatcher" + local utl = require "luci.util" + local sid = dsp.context.authsession + + if sid then + utl.ubus("session", "destroy", { ubus_rpc_session = sid }) + + luci.http.header("Set-Cookie", "sysauth=%s; expires=%s; path=%s" %{ + '', 'Thu, 01 Jan 1970 01:00:00 GMT', dsp.build_url() + }) + end + + luci.http.redirect(dsp.build_url()) +end + +function action_translations(lang) + local i18n = require "luci.i18n" + local http = require "luci.http" + local fs = require "nixio".fs + + if lang and #lang > 0 then + lang = i18n.setlanguage(lang) + if lang then + local s = fs.stat("%s/base.%s.lmo" %{ i18n.i18ndir, lang }) + if s then + http.header("Cache-Control", "public, max-age=31536000") + http.header("ETag", "%x-%x-%x" %{ s["ino"], s["size"], s["mtime"] }) + end + end + end + + http.prepare_content("application/javascript; charset=utf-8") + http.write("window.TR=") + http.write_json(i18n.dump()) +end + +local function ubus_reply(id, data, code, errmsg) + local reply = { jsonrpc = "2.0", id = id } + if errmsg then + reply.error = { + code = code, + message = errmsg + } + elseif type(code) == "table" then + reply.result = code + else + reply.result = { code, data } + end + + return reply +end + +local ubus_types = { + nil, + "array", + "object", + "string", + nil, -- INT64 + "number", + nil, -- INT16, + "boolean", + "double" +} + +local function ubus_access(sid, obj, fun) + local res, code = luci.util.ubus("session", "access", { + ubus_rpc_session = sid, + scope = "ubus", + object = obj, + ["function"] = fun + }) + + return (type(res) == "table" and res.access == true) +end + +local function ubus_request(req) + if type(req) ~= "table" or type(req.method) ~= "string" or req.jsonrpc ~= "2.0" or req.id == nil then + return ubus_reply(nil, nil, -32600, "Invalid request") + + elseif req.method == "call" then + if type(req.params) ~= "table" or #req.params < 3 then + return ubus_reply(nil, nil, -32600, "Invalid parameters") + end + + local sid, obj, fun, arg = + req.params[1], req.params[2], req.params[3], req.params[4] or {} + if type(arg) ~= "table" or arg.ubus_rpc_session ~= nil then + return ubus_reply(req.id, nil, -32602, "Invalid parameters") + end + + if sid == "00000000000000000000000000000000" and luci.dispatcher.context.authsession then + sid = luci.dispatcher.context.authsession + end + + if not ubus_access(sid, obj, fun) then + return ubus_reply(req.id, nil, -32002, "Access denied") + end + + arg.ubus_rpc_session = sid + + local res, code = luci.util.ubus(obj, fun, arg) + return ubus_reply(req.id, res, code or 0) + + elseif req.method == "list" then + if req.params == nil or (type(req.params) == "table" and #req.params == 0) then + local objs = luci.util.ubus() + return ubus_reply(req.id, nil, objs) + + elseif type(req.params) == "table" then + local n, rv = nil, {} + for n = 1, #req.params do + if type(req.params[n]) ~= "string" then + return ubus_reply(req.id, nil, -32602, "Invalid parameters") + end + + local sig = luci.util.ubus(req.params[n]) + if sig and type(sig) == "table" then + rv[req.params[n]] = {} + + local m, p + for m, p in pairs(sig) do + if type(p) == "table" then + rv[req.params[n]][m] = {} + + local pn, pt + for pn, pt in pairs(p) do + rv[req.params[n]][m][pn] = ubus_types[pt] or "unknown" + end + end + end + end + end + return ubus_reply(req.id, nil, rv) + + else + return ubus_reply(req.id, nil, -32602, "Invalid parameters") + end + end + + return ubus_reply(req.id, nil, -32601, "Method not found") +end + +function action_ubus() + local parser = require "luci.jsonc".new() + + luci.http.context.request:setfilehandler(function(_, s) + if not s then + return nil + end + + local ok, err = parser:parse(s) + return (not err or nil) + end) + + luci.http.context.request:content() + + local json = parser:get() + if json == nil or type(json) ~= "table" then + luci.http.prepare_content("application/json") + luci.http.write_json(ubus_reply(nil, nil, -32700, "Parse error")) + return + end + + local response + if #json == 0 then + response = ubus_request(json) + else + response = {} + + local _, request + for _, request in ipairs(json) do + response[_] = ubus_request(request) + end + end + + luci.http.prepare_content("application/json") + luci.http.write_json(response) +end + +function action_menu() + local dsp = require "luci.dispatcher" + local utl = require "luci.util" + local http = require "luci.http" + + local acls = utl.ubus("session", "access", { ubus_rpc_session = http.getcookie("sysauth") }) + local menu = dsp.menu_json(acls or {}) or {} + + http.prepare_content("application/json") + http.write_json(menu) +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.luac new file mode 100644 index 000000000000..da77eb8d59a2 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.lua new file mode 100644 index 000000000000..7aad10d58a28 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.lua @@ -0,0 +1,70 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2010-2019 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +module("luci.controller.admin.uci", package.seeall) + +local function ubus_state_to_http(errstr) + local map = { + ["Invalid command"] = 400, + ["Invalid argument"] = 400, + ["Method not found"] = 404, + ["Entry not found"] = 404, + ["No data"] = 204, + ["Permission denied"] = 403, + ["Timeout"] = 504, + ["Not supported"] = 500, + ["Unknown error"] = 500, + ["Connection failed"] = 503 + } + + local code = map[errstr] or 200 + local msg = errstr or "OK" + + luci.http.status(code, msg) + + if code ~= 204 then + luci.http.prepare_content("text/plain") + luci.http.write(msg) + end +end + +function action_apply_rollback() + local uci = require "luci.model.uci" + local token, errstr = uci:apply(true) + if token then + luci.http.prepare_content("application/json") + luci.http.write_json({ token = token }) + else + ubus_state_to_http(errstr) + end +end + +function action_apply_unchecked() + local uci = require "luci.model.uci" + local _, errstr = uci:apply(false) + ubus_state_to_http(errstr) +end + +function action_confirm() + local uci = require "luci.model.uci" + local token = luci.http.formvalue("token") + local _, errstr = uci:confirm(token) + ubus_state_to_http(errstr) +end + +function action_revert() + local uci = require "luci.model.uci" + local changes = uci:changes() + + -- Collect files to be reverted + local _, errstr, r, tbl + for r, tbl in pairs(changes) do + _, errstr = uci:revert(r) + if errstr then + break + end + end + + ubus_state_to_http(errstr or "OK") +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.luac new file mode 100644 index 000000000000..e56cfa114a3c Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.lua new file mode 100755 index 000000000000..a860f40b9b2c --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.lua @@ -0,0 +1,54 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. +local luci = {} +luci.util = require "luci.util" +luci.http = require "luci.http" + +module("luci.controller.hwnat", package.seeall) +function read_pipe(pipe) + local fp = io.popen(pipe) + local txt = fp:read("*a") + fp:close() + return txt +end + +function index() + if not (nixio.fs.access("/etc/config/hwnat") or nixio.fs.access("/etc/config/hwnat")) then + return + end + + entry({"admin", "mtk", "hwnat"}, cbi("hwnat"), _("NAT Accelerate")) + entry({"admin", "mtk", "hwnat_binding_status"}, call("hwnat_binding_status"), nil).leaf = true +end + +function hwnat_binding_status() + local result = luci.util.execi("hwnat -g") + local data = {} + local t = {} + local proto_num = {} + local i = 0 + + luci.http.prepare_content("application/json") + + if not result then + luci.http.write('[]') + return + end + + for line in result do + if i ~= 0 and line:match(":") then + t = line:split(" ") + proto_num = t[1]:split("=") + data[#data+1] = { + type_ = proto_num[1], + foe_entry = proto_num[2], + src_info = t[3], + new_info = t[5] + } + end + i = i + 1 + end + + luci.http.write_json(data) +end \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.luac new file mode 100644 index 000000000000..5681b9f31879 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.lua new file mode 100755 index 000000000000..e44ef5344910 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.lua @@ -0,0 +1,72 @@ +local luci = {} +luci.util = require "luci.util" +luci.http = require "luci.http" +local uci = require "luci.model.uci" + +module("luci.controller.ipsec", package.seeall) + +function index() + entry({"admin", "network", "ipsec"}, cbi("ipsec"), _("IP Security")) + entry({"admin", "network", "ipsec", "vpn_status"}, call("ipsec_vpn_status"), nil).leaf = true + entry({"admin", "network", "ipsec", "vpn_connect"}, call("ipsec_vpn_connect"), nil).leaf = true + entry({"admin", "network", "ipsec", "vpn_disconnect"}, call("ipsec_vpn_disconnect"), nil).leaf = true +end + +function ipsec_vpn_status() + local handle = io.popen(" ipsec status 2>/dev/null") + local result = handle:read("*all") + handle:close() + local obj ={} + + luci.http.prepare_content("application/json") + if result == "" then + obj.status = "Disconnected" + obj.msg = "Disconnected/Command not found" + luci.http.write_json(obj) + return + end + + a = string.match(result, "(%d+) up") + if (tonumber(a) == 0) then + b = string.match(result, "(%d) connecting") + if (tonumber(b) == 0) then + obj.status = "Disconnected" + obj.msg = "Disconnected" + luci.http.write_json(obj) + return + end + obj.status = "Connected" + obj.msg = b.." connecting" + luci.http.write_json(obj) + return + end + for line in result:gmatch("([^\n]*)\n?") do + if (string.find(line, "ESTABLISHED") or string.find(line, "DELETING")) then + obj.status = "Connected" + obj.msg = string.match(line, ": (.*),") + luci.http.write_json(obj) + end + end +end + +function ipsec_vpn_connect() + local l_gw_name = "" + local curs = uci.cursor() + curs:foreach("ipsec", "remote", function(s) l_gw_name = s[".name"] end) + l_subnet = curs:get("ipsec", "TUNNEL", "local_subnet") + l_wan = curs:get("network","wan" ,"device") + + luci.util.execi("iptables -t nat -I POSTROUTING -o "..l_wan.." -s "..l_subnet.." -j ACCEPT") + luci.util.execi("ipsec down "..l_gw_name.."-TUNNEL") + luci.util.execi("ipsec up "..l_gw_name.."-TUNNEL") + ipsec_vpn_status() +end + +function ipsec_vpn_disconnect() + local l_gw_name = "" + local curs = uci.cursor() + curs:foreach("ipsec", "remote", function(s) l_gw_name = s[".name"] end) + + luci.util.execi("ipsec down "..l_gw_name.."-TUNNEL") + ipsec_vpn_status() +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.luac new file mode 100644 index 000000000000..e388c475979d Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.lua new file mode 100755 index 000000000000..1e62e522b8f0 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.lua @@ -0,0 +1,2738 @@ +-- This module is a demo to configure MTK' proprietary WiFi driver. +-- Basic idea is to bypass uci and edit wireless profile (mt76xx.dat) directly. +-- LuCI's WiFi configuration is more logical and elegent, but it's quite tricky to +-- translate uci into MTK's WiFi profile (like we did in "uci2dat"). +-- And you will get your hands dirty. +-- +-- Hua Shao + +package.path = '/lib/wifi/?.lua;'..package.path +module("luci.controller.mtkwifi", package.seeall) +local onboardingType = 0; +local ioctl_help = require "ioctl_helper" +local map_help +if pcall(require, "map_helper") then + map_help = require "map_helper" +end +local http = require("luci.http") +local mtkwifi = require("mtkwifi") + +local logDisable = 1 +function debug_write(...) + -- luci.http.write(...) + if logDisable == 1 then + return + end + local syslog_msg = ""; + local ff = io.open("/tmp/dbgmsg", "a") + local nargs = select('#',...) + + for n=1, nargs do + local v = select(n,...) + if (type(v) == "string" or type(v) == "number") then + ff:write(v.." ") + syslog_msg = syslog_msg..v.." "; + elseif (type(v) == "boolean") then + if v then + ff:write("true ") + syslog_msg = syslog_msg.."true "; + else + ff:write("false ") + syslog_msg = syslog_msg.."false "; + end + elseif (type(v) == "nil") then + ff:write("nil ") + syslog_msg = syslog_msg.."nil "; + else + ff:write(" ") + syslog_msg = syslog_msg.." "; + end + end + ff:write("\n") + ff:close() + nixio.syslog("debug", syslog_msg) +end + +function index() + -- if not nixio.fs.access("/etc/wireless") then + -- return + -- end + + entry({"admin", "mtk"}, firstchild(), _("MTK"), 80) + entry({"admin", "mtk", "test"}, call("test")) + entry({"admin", "mtk", "wifi"}, template("admin_mtk/mtk_wifi_overview"), _("WiFi configuration"), 1) + entry({"admin", "mtk", "wifi", "chip_cfg_view"}, template("admin_mtk/mtk_wifi_chip_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "chip_cfg"}, call("chip_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "dev_cfg_view"}, template("admin_mtk/mtk_wifi_dev_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "dev_cfg"}, call("dev_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "dev_cfg_raw"}, call("dev_cfg_raw")).leaf = true + entry({"admin", "mtk", "wifi", "vif_cfg_view"}, template("admin_mtk/mtk_wifi_vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_cfg"}, call("vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_add_view"}, template("admin_mtk/mtk_wifi_vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_add"}, call("vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_del"}, call("vif_del")).leaf = true + entry({"admin", "mtk", "wifi", "vif_disable"}, call("vif_disable")).leaf = true + entry({"admin", "mtk", "wifi", "vif_enable"}, call("vif_enable")).leaf = true + entry({"admin", "mtk", "wifi", "get_station_list"}, call("get_station_list")) + entry({"admin", "mtk", "wifi", "get_country_region_list"}, call("get_country_region_list")).leaf = true + entry({"admin", "mtk", "wifi", "get_channel_list"}, call("get_channel_list")) + entry({"admin", "mtk", "wifi", "get_HT_ext_channel_list"}, call("get_HT_ext_channel_list")) + entry({"admin", "mtk", "wifi", "get_5G_2nd_80Mhz_channel_list"}, call("get_5G_2nd_80Mhz_channel_list")) + entry({"admin", "mtk", "wifi", "reset"}, call("reset_wifi")).leaf = true + entry({"admin", "mtk", "wifi", "reload"}, call("reload_wifi")).leaf = true + entry({"admin", "mtk", "wifi", "get_raw_profile"}, call("get_raw_profile")) + entry({"admin", "mtk", "wifi", "apcli_cfg_view"}, template("admin_mtk/mtk_wifi_apcli")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_cfg"}, call("apcli_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_disconnect"}, call("apcli_disconnect")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_connect"}, call("apcli_connect")).leaf = true + entry({"admin", "mtk", "netmode", "net_cfg"}, call("net_cfg")) + entry({"admin", "mtk", "console"}, template("admin_mtk/mtk_web_console"), _("Web Console"), 4) + entry({"admin", "mtk", "webcmd"}, call("webcmd")) + -- entry({"admin", "mtk", "man"}, template("admin_mtk/mtk_wifi_man"), _("M.A.N"), 3) + -- entry({"admin", "mtk", "man", "cfg"}, call("man_cfg")) + entry({"admin", "mtk", "wifi", "get_wps_info"}, call("get_WPS_Info")).leaf = true + entry({"admin", "mtk", "wifi", "get_wifi_pin"}, call("get_wifi_pin")).leaf = true + entry({"admin", "mtk", "wifi", "set_wifi_gen_pin"}, call("set_wifi_gen_pin")).leaf = true + entry({"admin", "mtk", "wifi", "set_wifi_wps_oob"}, call("set_wifi_wps_oob")).leaf = true + entry({"admin", "mtk", "wifi", "set_wifi_do_wps"}, call("set_wifi_do_wps")).leaf = true + entry({"admin", "mtk", "wifi", "get_wps_security"}, call("get_wps_security")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_get_wps_status"}, call("apcli_get_wps_status")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_do_enr_pin_wps"}, call("apcli_do_enr_pin_wps")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_do_enr_pbc_wps"}, call("apcli_do_enr_pbc_wps")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_cancel_wps"}, call("apcli_cancel_wps")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_wps_gen_pincode"}, call("apcli_wps_gen_pincode")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_wps_get_pincode"}, call("apcli_wps_get_pincode")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_scan"}, call("apcli_scan")).leaf = true; + entry({"admin", "mtk", "wifi", "sta_info"}, call("sta_info")).leaf = true; + entry({"admin", "mtk", "wifi", "get_apcli_conn_info"}, call("get_apcli_conn_info")).leaf = true; + entry({"admin", "mtk", "wifi", "apply_power_boost_settings"}, call("apply_power_boost_settings")).leaf = true; + entry({"admin", "mtk", "wifi", "apply_reboot"}, template("admin_mtk/mtk_wifi_apply_reboot")).leaf = true; + entry({"admin", "mtk", "wifi", "reboot"}, call("exec_reboot")).leaf = true; + entry({"admin", "mtk", "wifi", "get_bssid_num"}, call("get_bssid_num")).leaf = true; + entry({"admin", "mtk", "wifi", "loading"}, template("admin_mtk/mtk_wifi_loading")).leaf = true; + entry({"admin", "mtk", "wifi", "get_apply_status"}, call("get_apply_status")).leaf = true; + entry({"admin", "mtk", "wifi", "reset_to_defaults"}, call("reset_to_defaults")).leaf = true; + local mtkwifi = require("mtkwifi") + -- local profiles = mtkwifi.search_dev_and_profile() + -- for devname,profile in pairs(profiles) do + -- local cfgs = mtkwifi.load_profile(profile) + -- if cfgs["VOW_Airtime_Fairness_En"] then + -- entry({"admin", "mtk", "vow"}, template("admin_mtk/mtk_vow"), _("VoW / ATF / ATC"), 4) + -- break + -- end + -- end + + -- Define map_help again here as same defination at top does not come under scope of luci library. + local map_help + if pcall(require, "map_helper") then + map_help = require "map_helper" + end + if map_help then + entry({"admin", "mtk", "multi_ap", "reset_to_default_easymesh"}, call("reset_to_default_easymesh")).leaf = true; + entry({"admin", "mtk", "multi_ap"}, template("admin_mtk/mtk_wifi_multi_ap"), _("EasyMesh"), 5); + entry({"admin", "mtk", "multi_ap", "map_cfg"}, call("map_cfg")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_device_role"}, call("get_device_role")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_mandate_steering_on_agent"}, call("trigger_mandate_steering_on_agent")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_back_haul_steering_on_agent"}, call("trigger_back_haul_steering_on_agent")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_wps_fh_agent"}, call("trigger_wps_fh_agent")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_runtime_topology"}, template("admin_mtk/mtk_wifi_map_runtime_topology")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_runtime_topology"}, call("get_runtime_topology")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_data_element"}, template("admin_mtk/mtk_wifi_map_data_element")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_channel_scan_result"}, template("admin_mtk/mtk_wifi_map_channel_scan_result")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_channel_planning_score"}, template("admin_mtk/mtk_wifi_map_channel_planning_score")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_multi_ap_on_boarding"}, call("trigger_multi_ap_on_boarding")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_client_capabilities"}, template("admin_mtk/mtk_wifi_map_client_capabilities")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_client_capabilities"}, call("get_client_capabilities")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_ap_capabilities"}, template("admin_mtk/mtk_wifi_map_ap_capabilities")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_uplink_ap_selection"}, call("trigger_uplink_ap_selection")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_bh_connection_status"}, call("get_bh_connection_status")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_sta_steering_progress"}, call("get_sta_steering_progress")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_al_mac"}, call("get_al_mac")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_wifi_bh_priority"}, call("apply_wifi_bh_priority")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_ap_steer_rssi_th"}, call("apply_ap_steer_rssi_th")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_channel_utilization_th"}, call("apply_channel_utilization_th")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_sta_bh_interface"}, call("get_sta_bh_interface")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_ap_bh_inf_list"}, call("get_ap_bh_inf_list")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_ap_fh_inf_list"}, call("get_ap_fh_inf_list")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_fh_status_bss"}, template("admin_mtk/mtk_wifi_map_bssinfo")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_bh_link_metrics_ctrler"}, template("admin_mtk/mtk_wifi_map_bh_link_metrics")).leaf = true; + entry({"admin", "mtk", "multi_ap", "easymesh_bss_config_renew"}, template("admin_mtk/mtk_wifi_map_bss_cfg_renew")).leaf = true; + entry({"admin", "mtk", "multi_ap", "easymesh_bss_cfg"}, call("easymesh_bss_cfg")).leaf = true; + entry({"admin", "mtk", "multi_ap", "validate_add_easymesh_bss_req"}, call("validate_add_easymesh_bss_req")).leaf = true; + entry({"admin", "mtk", "multi_ap", "remove_easymesh_bss_cfg_req"}, call("remove_easymesh_bss_cfg_req")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_easymesh_bss_cfg"}, call("apply_easymesh_bss_cfg")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_force_ch_switch"}, call("apply_force_ch_switch")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_user_preferred_channel"}, call("apply_user_preferred_channel")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_channel_planning_r2"}, call("trigger_channel_planning_r2")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_de_dump"}, call("trigger_de_dump")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_data_element"}, call("get_data_element")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_channel_scan"}, call("trigger_channel_scan")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_channel_stats"}, call("get_channel_stats")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_channel_planning_score"}, call("get_channel_planning_score")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_user_preferred_channel"}, call("get_user_preferred_channel")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_sp_rule_list"}, call("get_sp_rule_list")).leaf = true; + entry({"admin", "mtk", "multi_ap", "del_sp_rule"}, call("del_sp_rule")).leaf = true; + entry({"admin", "mtk", "multi_ap", "sp_rule_reorder"}, call("sp_rule_reorder")).leaf = true; + entry({"admin", "mtk", "multi_ap", "sp_rule_move"}, call("sp_rule_move")).leaf = true; + entry({"admin", "mtk", "multi_ap", "add_sp_rule"}, call("sp_rule_add")).leaf = true; + entry({"admin", "mtk", "multi_ap", "sp_config_done"}, call("sp_config_done")).leaf = true; + entry({"admin", "mtk", "multi_ap", "submit_dpp_uri"}, call("submit_dpp_uri")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_bootstrapping_uri"}, template("admin_mtk/mtk_wifi_map_display_bootstrapping_uri")).leaf = true; + entry({"admin", "mtk", "multi_ap", "start_dpp_onboarding"}, call("start_dpp_onboarding")).leaf = true; + entry({"admin", "mtk", "multi_ap", "generate_dpp_uri"}, call("generate_dpp_uri")).leaf = true; + entry({"admin", "mtk", "multi_ap", "retrive_dpp_uri"}, call("retrive_dpp_uri")).leaf = true; + end +end + +function test() + http.write_json(http.formvalue()) +end + +function exec_reboot() + os.execute("rm -f /tmp/mtk/wifi/reboot_required >/dev/null 2>&1") + os.execute("sync >/dev/null 2>&1") + os.execute("reboot >/dev/null 2>&1") +end + +function get_apply_status() + local ret = {} + + if mtkwifi.is_child_active() then + ret["status"] = "ON_PROGRESS" + elseif mtkwifi.exists("/tmp/mtk/wifi/reboot_required") then + -- If the "wifi restart" command can not re-install the driver; then, it will create + -- "/tmp/mtk/wifi/reboot_required" file to indicate LuCI that the settings will be applied + -- only after reboot of the device. + -- Redirect "Reboot Device" web-page to get consent from the user to reboot the device. + ret["status"] = "REBOOT" + else + ret["status"] = "DONE" + end + http.write_json(ret) +end + +function __mtkwifi_save_profile(cfgs, path, isProfileSettingsAppliedToDriver) + -- Create the applied settings backup file before saving the new profile settings only if it does not exist. + if not mtkwifi.exists(mtkwifi.__profile_applied_settings_path(path)) then + os.execute("cp -f "..path.." "..mtkwifi.__profile_applied_settings_path(path)) + end + if isProfileSettingsAppliedToDriver then + -- It means the some context based profile settings to be saved in DAT file is already applied to the driver. + -- Find the profile settings which are not applied to the driver before saving the new profile settings + local diff = mtkwifi.diff_profile(path) + mtkwifi.save_profile(cfgs, path) + -- If there are any settings which are not applied to the driver, then do NOT copy and WebUI will display the "need reload to apply changes" message + -- Otherwise, copy the new profile settings and WebUI will NOT display the "need reload to apply changes" message + if next(diff) == nil then + os.execute("cp -f "..path.." "..mtkwifi.__profile_applied_settings_path(path)) + end + else + mtkwifi.save_profile(cfgs, path) + end +end + +local __mtkwifi_reload = function (devname) + local wifi_restart = false + local wifi_reload = false + local profiles = mtkwifi.search_dev_and_profile() + + for dev,profile in pairs(profiles) do + if not devname or devname == dev then + local diff = mtkwifi.diff_profile(profile) + local diff_easy = mtkwifi.diff_profile(mtkwifi.__write_easymesh_profile_path(), mtkwifi.__profile_applied_settings_path(mtkwifi.__write_easymesh_profile_path())) + if not next(diff) and not next(diff_easy) then return end + __process_settings_before_apply(dev, profile, diff) + + if diff.BssidNum or diff.WHNAT or diff.E2pAccessMode or diff.HT_RxStream or diff.HT_TxStream or diff.HE_LDPC or diff.WdsEnable then + -- Addition or deletion of a vif requires re-installation of the driver. + -- Change in WHNAT setting also requires re-installation of the driver. + -- Driver will be re-installed by "wifi restart" command. + wifi_restart = true + else + wifi_reload = true + end + + end + end + + if wifi_restart then + os.execute("wifi restart "..(devname or "")) + debug_write("wifi restart "..(devname or "")) + elseif wifi_reload then + os.execute("wifi reload "..(devname or "")) + debug_write("wifi reload "..(devname or "")) + end + + for dev,profile in pairs(profiles) do + if not devname or devname == dev then + -- keep a backup for this commit + -- it will be used in mtkwifi.diff_profile() + os.execute("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + debug_write("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + end + end + + if map_help then + local easymesh_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__read_easymesh_profile_path()) + os.execute("cp -f "..mtkwifi.__read_easymesh_profile_path().." "..easymesh_applied_path) + end +end + +function __process_settings_before_apply(devname, profile, diff) + local devs = mtkwifi.get_all_devs() + local cfgs = mtkwifi.load_profile(profile) + __apply_wifi_wpsconf(devs, devname, cfgs, diff) +end + +function chip_cfg(devname) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + local devs = mtkwifi.get_all_devs() + local dbdc_cfgs = {} + local dev = {} + dev = devs and devs[devname] + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "chip_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) == string.byte("_") then + nixio.syslog("err", "chip_cfg, special: "..k.."="..v) + else + if dev.dbdc == true then + dbdc_cfgs[k] = v or "" + else + cfgs[k] = v or "" + end + end + end + + -- VOW + -- ATC should actually be scattered into each SSID, but I'm just lazy. + if cfgs.VOW_Airtime_Fairness_En then + for i = 1,tonumber(cfgs.BssidNum) do + __atc_tp = http.formvalue("__atc_vif"..i.."_tp") or "0" + __atc_min_tp = http.formvalue("__atc_vif"..i.."_min_tp") or "0" + __atc_max_tp = http.formvalue("__atc_vif"..i.."_max_tp") or "0" + __atc_at = http.formvalue("__atc_vif"..i.."_at") or "0" + __atc_min_at = http.formvalue("__atc_vif"..i.."_min_at") or "0" + __atc_max_at = http.formvalue("__atc_vif"..i.."_max_at") or "0" + + nixio.syslog("info", "ATC.__atc_tp ="..i..__atc_tp ); + nixio.syslog("info", "ATC.__atc_min_tp ="..i..__atc_min_tp ); + nixio.syslog("info", "ATC.__atc_max_tp ="..i..__atc_max_tp ); + nixio.syslog("info", "ATC.__atc_at ="..i..__atc_at ); + nixio.syslog("info", "ATC.__atc_min_at ="..i..__atc_min_at ); + nixio.syslog("info", "ATC.__atc_max_at ="..i..__atc_max_at ); + + dbdc_cfgs.VOW_Rate_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Rate_Ctrl_En, i, __atc_tp) + dbdc_cfgs.VOW_Group_Min_Rate = mtkwifi.token_set(cfgs.VOW_Group_Min_Rate, i, __atc_min_tp) + dbdc_cfgs.VOW_Group_Max_Rate = mtkwifi.token_set(cfgs.VOW_Group_Max_Rate, i, __atc_max_tp) + + dbdc_cfgs.VOW_Airtime_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Airtime_Ctrl_En, i, __atc_at) + dbdc_cfgs.VOW_Group_Min_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Min_Ratio, i, __atc_min_at) + dbdc_cfgs.VOW_Group_Max_Ratiio = mtkwifi.token_set(cfgs.VOW_Group_Max_Ratio, i, __atc_max_at) + + cfgs.VOW_Rate_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Rate_Ctrl_En, i, __atc_tp) + cfgs.VOW_Group_Min_Rate = mtkwifi.token_set(cfgs.VOW_Group_Min_Rate, i, __atc_min_tp) + cfgs.VOW_Group_Max_Rate = mtkwifi.token_set(cfgs.VOW_Group_Max_Rate, i, __atc_max_tp) + + cfgs.VOW_Airtime_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Airtime_Ctrl_En, i, __atc_at) + cfgs.VOW_Group_Min_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Min_Ratio, i, __atc_min_at) + cfgs.VOW_Group_Max_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Max_Ratio, i, __atc_max_at) + + end + + dbdc_cfgs.VOW_RX_En = http.formvalue("VOW_RX_En") or "0" + cfgs.VOW_RX_En = http.formvalue("VOW_RX_En") or "0" + end + + if dev.dbdc == true then + for devname, profile in pairs(profiles) do + __mtkwifi_save_profile(dbdc_cfgs, profile, false) + end + else + __mtkwifi_save_profile(cfgs, profiles[devname], false) + end + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "chip_cfg_view",devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "chip_cfg_view",devname)) + end + +end + +function dev_cfg(devname) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "dev_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) == string.byte("_") then + nixio.syslog("err", "dev_cfg, special: "..k.."="..v) + else + cfgs[k] = v or "" + end + end + + if cfgs.Channel == "0" then -- Auto Channel Select + cfgs.AutoChannelSelect = "3" + else + cfgs.AutoChannelSelect = "0" + end + + if http.formvalue("__bw") == "20" then + cfgs.HT_BW = 0 + cfgs.VHT_BW = 0 + elseif http.formvalue("__bw") == "40" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 0 + cfgs.HT_BSSCoexistence = 0 + elseif http.formvalue("__bw") == "60" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 0 + cfgs.HT_BSSCoexistence = 1 + elseif http.formvalue("__bw") == "80" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 1 + elseif http.formvalue("__bw") == "160" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 2 + elseif http.formvalue("__bw") == "161" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 3 + cfgs.VHT_Sec80_Channel = http.formvalue("VHT_Sec80_Channel") or "" + end + + if mtkwifi.band(string.split(cfgs.WirelessMode,";")[1]) == "5G" or mtkwifi.band(cfgs.WirelessMode) == "6G" then + cfgs.CountryRegionABand = http.formvalue("__cr"); + else + cfgs.CountryRegion = http.formvalue("__cr"); + end + + if http.formvalue("TxPower") then + local txpower = tonumber(http.formvalue("TxPower")) + if txpower < 100 then + cfgs.PERCENTAGEenable=1 + else + cfgs.PERCENTAGEenable=0 + end + end + + local IndividualTWTSupport = tonumber(http.formvalue("IndividualTWTSupport")) + if IndividualTWTSupport == 0 then + cfgs.TWTResponder=0 + cfgs.TWTRequired=0 + elseif IndividualTWTSupport == 1 then + cfgs.TWTResponder=1 + cfgs.TWTRequired=0 + else + cfgs.TWTResponder=1 + cfgs.TWTRequired=1 + end + + local mimo = http.formvalue("__mimo") + if mimo == "0" then + cfgs.ETxBfEnCond=1 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=0 + elseif mimo == "1" then + cfgs.ETxBfEnCond=0 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=1 + elseif mimo == "2" then + cfgs.ETxBfEnCond=1 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=1 + elseif mimo == "3" then + cfgs.ETxBfEnCond=1 + if tonumber(cfgs.ApCliEnable) == 1 then + cfgs.MUTxRxEnable=3 + else + cfgs.MUTxRxEnable=1 + end + cfgs.ITxBfEn=0 + elseif mimo == "4" then + cfgs.ETxBfEnCond=1 + if tonumber(cfgs.ApCliEnable) == 1 then + cfgs.MUTxRxEnable=3 + else + cfgs.MUTxRxEnable=1 + end + cfgs.ITxBfEn=1 + else + cfgs.ETxBfEnCond=0 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=0 + end + +-- if cfgs.ApCliEnable == "1" then +-- cfgs.Channel = http.formvalue("__apcli_channel") +-- end + + -- WDS + -- http.write_json(http.formvalue()) + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "dev_cfg_view",devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "dev_cfg_view",devname)) + end +end + +function dev_cfg_raw(devname) + -- http.write_json(http.formvalue()) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + + local raw = http.formvalue("raw") + raw = string.gsub(raw, "\r\n", "\n") + local cfgs = mtkwifi.load_profile(nil, raw) + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "dev_cfg_view", devname)) +end + +function __delete_mbss_para(cfgs, vif_idx) + debug_write(vif_idx) + cfgs["WPAPSK"..vif_idx]="" + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vif_idx,"") + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vif_idx,"") + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vif_idx,"") + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vif_idx,"") + cfgs["RADIUS_Server"]=mtkwifi.token_set(cfgs["RADIUS_Server"],vif_idx,"") + cfgs["RADIUS_Port"]=mtkwifi.token_set(cfgs["RADIUS_Port"],vif_idx,"") + cfgs["RADIUS_Key"..vif_idx]="" + cfgs["DefaultKeyID"]=mtkwifi.token_set(cfgs["DefaultKeyID"],vif_idx,"") + cfgs["IEEE8021X"]=mtkwifi.token_set(cfgs["IEEE8021X"],vif_idx,"") + cfgs["WscConfMode"]=mtkwifi.token_set(cfgs["WscConfMode"],vif_idx,"") + cfgs["PreAuth"]=mtkwifi.token_set(cfgs["PreAuth"],vif_idx,"") + cfgs["HT_STBC"] = mtkwifi.token_set(cfgs["HT_STBC"],vif_idx,"") + cfgs["HT_LDPC"] = mtkwifi.token_set(cfgs["HT_LDPC"],vif_idx,"") + cfgs["VHT_STBC"] = mtkwifi.token_set(cfgs["VHT_STBC"],vif_idx,"") + cfgs["VHT_LDPC"] = mtkwifi.token_set(cfgs["VHT_LDPC"],vif_idx,"") + cfgs["HideSSID"]=mtkwifi.token_set(cfgs["HideSSID"],vif_idx,"") + cfgs["NoForwarding"]=mtkwifi.token_set(cfgs["NoForwarding"],vif_idx,"") + cfgs["WmmCapable"]=mtkwifi.token_set(cfgs["WmmCapable"],vif_idx,"") + cfgs["TxRate"]=mtkwifi.token_set(cfgs["TxRate"],vif_idx,"") + cfgs["RekeyInterval"]=mtkwifi.token_set(cfgs["RekeyInterval"],vif_idx,"") + cfgs["AuthMode"]=mtkwifi.token_set(cfgs["AuthMode"],vif_idx,"") + cfgs["EncrypType"]=mtkwifi.token_set(cfgs["EncrypType"],vif_idx,"") + cfgs["session_timeout_interval"]=mtkwifi.token_set(cfgs["session_timeout_interval"],vif_idx,"") + cfgs["WscModeOption"]=mtkwifi.token_set(cfgs["WscModeOption"],vif_idx,"") + cfgs["RekeyMethod"]=mtkwifi.token_set(cfgs["RekeyMethod"],vif_idx,"") + cfgs["PMFMFPC"] = mtkwifi.token_set(cfgs["PMFMFPC"],vif_idx,"") + cfgs["PMFMFPR"] = mtkwifi.token_set(cfgs["PMFMFPR"],vif_idx,"") + cfgs["PMFSHA256"] = mtkwifi.token_set(cfgs["PMFSHA256"],vif_idx,"") + cfgs["PMKCachePeriod"] = mtkwifi.token_set(cfgs["PMKCachePeriod"],vif_idx,"") + cfgs["Wapiifname"] = mtkwifi.token_set(cfgs["Wapiifname"],vif_idx,"") + cfgs["RRMEnable"] = mtkwifi.token_set(cfgs["RRMEnable"],vif_idx,"") + cfgs["DLSCapable"] = mtkwifi.token_set(cfgs["DLSCapable"],vif_idx,"") + cfgs["APSDCapable"] = mtkwifi.token_set(cfgs["APSDCapable"],vif_idx,"") + cfgs["FragThreshold"] = mtkwifi.token_set(cfgs["FragThreshold"],vif_idx,"") + cfgs["RTSThreshold"] = mtkwifi.token_set(cfgs["RTSThreshold"],vif_idx,"") + cfgs["VHT_SGI"] = mtkwifi.token_set(cfgs["VHT_SGI"],vif_idx,"") + cfgs["VHT_BW_SIGNAL"] = mtkwifi.token_set(cfgs["VHT_BW_SIGNAL"],vif_idx,"") + cfgs["HT_PROTECT"] = mtkwifi.token_set(cfgs["HT_PROTECT"],vif_idx,"") + cfgs["HT_GI"] = mtkwifi.token_set(cfgs["HT_GI"],vif_idx,"") + cfgs["HT_OpMode"] = mtkwifi.token_set(cfgs["HT_OpMode"],vif_idx,"") + cfgs["HT_TxStream"] = mtkwifi.token_set(cfgs["HT_TxStream"],vif_idx,"") + cfgs["HT_RxStream"] = mtkwifi.token_set(cfgs["HT_RxStream"],vif_idx,"") + cfgs["HT_AMSDU"] = mtkwifi.token_set(cfgs["HT_AMSDU"],vif_idx,"") + cfgs["HT_AutoBA"] = mtkwifi.token_set(cfgs["HT_AutoBA"],vif_idx,"") + cfgs["IgmpSnEnable"] = mtkwifi.token_set(cfgs["IgmpSnEnable"],vif_idx,"") + cfgs["WirelessMode"] = mtkwifi.token_set(cfgs["WirelessMode"],vif_idx,"") + cfgs["WdsEnable"] = mtkwifi.token_set(cfgs["WdsEnable"],vif_idx,"") + cfgs["MuOfdmaDlEnable"] = mtkwifi.token_set(cfgs["MuOfdmaDlEnable"],vif_idx,"") + cfgs["MuOfdmaUlEnable"] = mtkwifi.token_set(cfgs["MuOfdmaUlEnable"],vif_idx,"") + cfgs["MuMimoDlEnable"] = mtkwifi.token_set(cfgs["MuMimoDlEnable"],vif_idx,"") + cfgs["MuMimoUlEnable"] = mtkwifi.token_set(cfgs["MuMimoUlEnable"],vif_idx,"") + +end + +function vif_del(dev, vif) + debug_write("vif_del("..dev..vif..")") + local devname,vifname = dev, vif + debug_write("devname="..devname) + debug_write("vifname="..vifname) + local devs = mtkwifi.get_all_devs() + local idx = devs[devname]["vifs"][vifname].vifidx -- or tonumber(string.match(vifname, "%d+")) + 1 + debug_write("idx="..idx, devname, vifname) + local profile = devs[devname].profile + assert(profile) + if idx and tonumber(idx) >= 0 then + local cfgs = mtkwifi.load_profile(profile) + __delete_mbss_para(cfgs, idx) + if cfgs then + debug_write("ssid"..idx.."="..cfgs["SSID"..idx].."
    ") + cfgs["SSID"..idx] = "" + debug_write("ssid"..idx.."="..cfgs["SSID"..idx].."
    ") + debug_write("wpapsk"..idx.."="..cfgs["WPAPSK"..idx].."
    ") + cfgs["WPAPSK"..idx] = "" + local ssidlist = {} + local j = 1 + for i = 1,16 do + if cfgs["SSID"..i] ~= "" then + ssidlist[j] = cfgs["SSID"..i] + j = j + 1 + end + end + for i,v in ipairs(ssidlist) do + debug_write("ssidlist"..i.."="..v) + end + debug_write("cfgs.BssidNum="..cfgs.BssidNum.." #ssidlist="..#ssidlist) + assert(tonumber(cfgs.BssidNum) == #ssidlist + 1, "BssidNum="..cfgs.BssidNum.." SSIDlist="..#ssidlist..", BssidNum count does not match with SSIDlist count.") + cfgs.BssidNum = #ssidlist + for i = 1,16 do + if i <= cfgs.BssidNum then + cfgs["SSID"..i] = ssidlist[i] + elseif cfgs["SSID"..i] then + cfgs["SSID"..i] = "" + end + end + + __mtkwifi_save_profile(cfgs, profile, false) + else + debug_write(profile.." cannot be found!") + end + end + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function vif_disable(iface) + os.execute("ifconfig "..iface.." down") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function vif_enable(iface) + os.execute("ifconfig "..iface.." up") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + + +--[[ +-- security config in mtk wifi is quite complicated! +-- cfgs listed below are attached with vif and combined like "0;0;0;0". They need specicial treatment. + TxRate, WmmCapable, NoForwarding, + HideSSID, IEEE8021X, PreAuth, + AuthMode, EncrypType, RekeyMethod, + RekeyInterval, PMKCachePeriod, + DefaultKeyId, Key{n}Type, HT_EXTCHA, + RADIUS_Server, RADIUS_Port, +]] + +local function conf_wep_keys(cfgs,vifidx) + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, vifidx, http.formvalue("__DefaultKeyID") or 1) + cfgs["Key1Str"..vifidx] = http.formvalue("Key1Str"..vifidx) + cfgs["Key2Str"..vifidx] = http.formvalue("Key2Str"..vifidx) + cfgs["Key3Str"..vifidx] = http.formvalue("Key3Str"..vifidx) + cfgs["Key4Str"..vifidx] = http.formvalue("Key4Str"..vifidx) + + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vifidx, http.formvalue("WEP1Type"..vifidx)) + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vifidx, http.formvalue("WEP2Type"..vifidx)) + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vifidx, http.formvalue("WEP3Type"..vifidx)) + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vifidx, http.formvalue("WEP4Type"..vifidx)) + + return cfgs +end + +local function __security_cfg(cfgs, vif_idx) + debug_write("__security_cfg, before, HideSSID="..tostring(cfgs.HideSSID)) + debug_write("__security_cfg, before, NoForwarding="..tostring(cfgs.NoForwarding)) + debug_write("__security_cfg, before, WmmCapable="..tostring(cfgs.WmmCapable)) + debug_write("__security_cfg, before, TxRate="..tostring(cfgs.TxRate)) + debug_write("__security_cfg, before, RekeyInterval="..tostring(cfgs.RekeyInterval)) + debug_write("__security_cfg, before, AuthMode="..tostring(cfgs.AuthMode)) + debug_write("__security_cfg, before, EncrypType="..tostring(cfgs.EncrypType)) + debug_write("__security_cfg, before, WscModeOption="..tostring(cfgs.WscModeOption)) + debug_write("__security_cfg, before, RekeyMethod="..tostring(cfgs.RekeyMethod)) + debug_write("__security_cfg, before, IEEE8021X="..tostring(cfgs.IEEE8021X)) + debug_write("__security_cfg, before, DefaultKeyID="..tostring(cfgs.DefaultKeyID)) + debug_write("__security_cfg, before, PMFMFPC="..tostring(cfgs.PMFMFPC)) + debug_write("__security_cfg, before, PMFMFPR="..tostring(cfgs.PMFMFPR)) + debug_write("__security_cfg, before, PMFSHA256="..tostring(cfgs.PMFSHA256)) + debug_write("__security_cfg, before, RADIUS_Server="..tostring(cfgs.RADIUS_Server)) + debug_write("__security_cfg, before, RADIUS_Port="..tostring(cfgs.RADIUS_Port)) + debug_write("__security_cfg, before, session_timeout_interval="..tostring(cfgs.session_timeout_interval)) + debug_write("__security_cfg, before, PMKCachePeriod="..tostring(cfgs.PMKCachePeriod)) + debug_write("__security_cfg, before, PreAuth="..tostring(cfgs.PreAuth)) + debug_write("__security_cfg, before, Wapiifname="..tostring(cfgs.Wapiifname)) + + -- Reset/Clear all necessary settings here. Later, these settings will be set as per AuthMode. + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "DISABLE") + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, vif_idx, "0") + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + -- Update the settings which are not dependent on AuthMode + cfgs.HideSSID = mtkwifi.token_set(cfgs.HideSSID, vif_idx, http.formvalue("__hidessid") or "0") + cfgs.NoForwarding = mtkwifi.token_set(cfgs.NoForwarding, vif_idx, http.formvalue("__noforwarding") or "0") + cfgs.WmmCapable = mtkwifi.token_set(cfgs.WmmCapable, vif_idx, http.formvalue("__wmmcapable") or "0") + cfgs.TxRate = mtkwifi.token_set(cfgs.TxRate, vif_idx, http.formvalue("__txrate") or "0"); + cfgs.RekeyInterval = mtkwifi.token_set(cfgs.RekeyInterval, vif_idx, http.formvalue("__rekeyinterval") or "0"); + + local __authmode = http.formvalue("__authmode") or "Disable" + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, __authmode) + + if __authmode == "Disable" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OPEN") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "NONE") + + elseif __authmode == "OPEN" or __authmode == "SHARED" or __authmode == "WEPAUTO" then + cfgs.WscModeOption = "0" + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "WEP") + cfgs = conf_wep_keys(cfgs,vif_idx) + + elseif __authmode == "Enhanced Open" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OWE") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPAPSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + + elseif __authmode == "WPAPSKWPA2PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.WpaMixPairCipher = "WPA_TKIP_WPA2_AES" + + elseif __authmode == "WPA2PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, http.formvalue("__pmfmfpc") or "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, http.formvalue("__pmfmfpr") or "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, http.formvalue("__pmfsha256") or "0") + + elseif __authmode == "WPA3PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA2PSKWPA3PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA2" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, http.formvalue("__pmfmfpc") or "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, http.formvalue("__pmfmfpr") or "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, http.formvalue("__pmfsha256") or "0") + + elseif __authmode == "WPA3" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA3-192-bit" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "WPA3-192") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "GCMP256") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA1WPA2" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "1812") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + + elseif __authmode == "IEEE8021X" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OPEN") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__8021x_wep") and "WEP" or "NONE") + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, vif_idx, "1") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + + elseif __authmode == "WAICERT" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "SMS4") + cfgs.Wapiifname = mtkwifi.token_set(cfgs.Wapiifname, vif_idx, "br-lan") + -- cfgs.wapicert_asipaddr + -- cfgs.WapiAsPort + -- cfgs.wapicert_ascert + -- cfgs.wapicert_usercert + + elseif __authmode == "WAIPSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "SMS4") + -- cfgs.wapipsk_keytype + -- cfgs.wapipsk_prekey + end + + debug_write("__security_cfg, after, HideSSID="..tostring(cfgs.HideSSID)) + debug_write("__security_cfg, after, NoForwarding="..tostring(cfgs.NoForwarding)) + debug_write("__security_cfg, after, WmmCapable="..tostring(cfgs.WmmCapable)) + debug_write("__security_cfg, after, TxRate="..tostring(cfgs.TxRate)) + debug_write("__security_cfg, after, RekeyInterval="..tostring(cfgs.RekeyInterval)) + debug_write("__security_cfg, after, AuthMode="..tostring(cfgs.AuthMode)) + debug_write("__security_cfg, after, EncrypType="..tostring(cfgs.EncrypType)) + debug_write("__security_cfg, after, WscModeOption="..tostring(cfgs.WscModeOption)) + debug_write("__security_cfg, after, RekeyMethod="..tostring(cfgs.RekeyMethod)) + debug_write("__security_cfg, after, IEEE8021X="..tostring(cfgs.IEEE8021X)) + debug_write("__security_cfg, after, DefaultKeyID="..tostring(cfgs.DefaultKeyID)) + debug_write("__security_cfg, after, PMFMFPC="..tostring(cfgs.PMFMFPC)) + debug_write("__security_cfg, after, PMFMFPR="..tostring(cfgs.PMFMFPR)) + debug_write("__security_cfg, after, PMFSHA256="..tostring(cfgs.PMFSHA256)) + debug_write("__security_cfg, after, RADIUS_Server="..tostring(cfgs.RADIUS_Server)) + debug_write("__security_cfg, after, RADIUS_Port="..tostring(cfgs.RADIUS_Port)) + debug_write("__security_cfg, after, session_timeout_interval="..tostring(cfgs.session_timeout_interval)) + debug_write("__security_cfg, after, PMKCachePeriod="..tostring(cfgs.PMKCachePeriod)) + debug_write("__security_cfg, after, PreAuth="..tostring(cfgs.PreAuth)) + debug_write("__security_cfg, after, Wapiifname="..tostring(cfgs.Wapiifname)) +end + +function initialize_multiBssParameters(cfgs,vif_idx) + cfgs["WPAPSK"..vif_idx]="12345678" + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vif_idx,"0") + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vif_idx,"0") + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vif_idx,"0") + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vif_idx,"0") + cfgs["RADIUS_Server"]=mtkwifi.token_set(cfgs["RADIUS_Server"],vif_idx,"0") + cfgs["RADIUS_Port"]=mtkwifi.token_set(cfgs["RADIUS_Port"],vif_idx,"1812") + cfgs["RADIUS_Key"..vif_idx]="ralink" + cfgs["DefaultKeyID"]=mtkwifi.token_set(cfgs["DefaultKeyID"],vif_idx,"1") + cfgs["IEEE8021X"]=mtkwifi.token_set(cfgs["IEEE8021X"],vif_idx,"0") + cfgs["WscConfMode"]=mtkwifi.token_set(cfgs["WscConfMode"],vif_idx,"0") + cfgs["PreAuth"]=mtkwifi.token_set(cfgs["PreAuth"],vif_idx,"0") + return cfgs +end + +function __wps_ap_pbc_start_all(ifname) + os.execute("iwpriv "..ifname.." set WscMode=2"); + os.execute("iwpriv "..ifname.." set WscGetConf=1"); +end + +function __wps_ap_pin_start_all(ifname, pincode) + os.execute("iwpriv "..ifname.." set WscMode=1") + os.execute("iwpriv "..ifname.." set WscPinCode="..pincode) + os.execute("iwpriv "..ifname.." set WscGetConf=1") +end + +local __restart_miniupnpd = function (devName,ifName) + if pcall(require, "wifi_services") then + -- OpenWRT + assert(type(devName) == type("")) + assert(type(ifName) == type("")) + local wifi_service = require("wifi_services") + debug_write("Call miniupnpd_chk() of wifi_services module") + miniupnpd_chk(devName,ifName,wifi_service) + else + -- LSDK + debug_write("Execute miniupnpd.sh script!") + os.execute("miniupnpd.sh init") + end +end + +local __restart_hotspot_daemon = function () + os.execute("killall hs") + os.execute("rm -rf /tmp/hotspot*") + -- As this function is executed in child environment, there is no need to spawn it using fork-exec method. + os.execute("hs -d 1 -v 2 -f/etc_ro/hotspot_ap.conf") +end + +local __restart_8021x = function (devName,ifName) + if pcall(require, "wifi_services") then + -- OpenWRT + assert(type(devName) == type("")) + assert(type(ifName) == type("")) + local ifPrefix = string.match(ifName,"([a-z]+)") + assert(type(ifPrefix) == type("")) + local wifi_service = require("wifi_services") + debug_write("Call d8021xd_chk() of wifi_services module") + d8021xd_chk(devName,ifPrefix,ifPrefix.."0",true) + else + -- LSDK + debug_write("Call mtkwifi.restart_8021x()") + mtkwifi.restart_8021x(devName) + end +end + +--Landen: CP functions from wireless for Ajax, reloading page is not required when DBDC ssid changed. +local __restart_all_daemons = function (devName,ifName) + __restart_8021x(devName,ifName) + __restart_hotspot_daemon() + __restart_miniupnpd(devName,ifName) +end + +function __apply_wifi_wpsconf(devs, devname, cfgs, diff) + local saved = cfgs.WscConfMode and cfgs.WscConfMode:gsub(";-(%d);-","%1") or "" + local applied = diff.WscConfMode and diff["WscConfMode"][2]:gsub(";-(%d);-","%1") or "" + local num_ifs = tonumber(cfgs.BssidNum) or 0 + + for idx=1, num_ifs do + local ifname = devs[devname]["vifs"][idx]["vifname"] + if mtkwifi.__any_wsc_enabled(saved:sub(idx,idx)) == 1 then + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, idx, "2") + else + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, idx, "1") + end + if (diff.WscConfMode) and saved:sub(idx,idx) ~= applied:sub(idx,idx) then + cfgs = mtkwifi.__restart_if_wps(devname, ifname, cfgs) + end + end + + -- __mtkwifi_save_profile() is called outside the loop because it is a high time consuming function. + __mtkwifi_save_profile(cfgs, devs[devname]["profile"], false) + + if diff.WscConfMode then + for idx=1, num_ifs do + local ifname = devs[devname]["vifs"][idx]["vifname"] + if saved:sub(idx,idx) ~= applied:sub(idx,idx) then + __restart_miniupnpd(devname, ifname) + end + end + end +end + +function __set_wifi_wpsconf(cfgs, wsc_enable, vif_idx) + debug_write("__set_wifi_wpsconf : wsc_enable = ",wsc_enable) + if(wsc_enable == "1") then + cfgs["WscConfMode"] = mtkwifi.token_set(cfgs["WscConfMode"], vif_idx, "7") + else + cfgs["WscConfMode"] = mtkwifi.token_set(cfgs["WscConfMode"], vif_idx, "0") + end + if(((http.formvalue("__authmode")=="OPEN") and + (http.formvalue("__encrypttype") == "WEP")) or + (http.formvalue("__hidessid") == "1")) then + cfgs.WscConfMode = mtkwifi.token_set(cfgs.WscConfMode, vif_idx, "0") + end + debug_write("__set_wifi_wpsconf : WscConfMode = ",cfgs["WscConfMode"]) +end + +function __update_mbss_para(cfgs, vif_idx) + debug_write(vif_idx) + cfgs.HT_STBC = mtkwifi.token_set(cfgs.HT_STBC, vif_idx, http.formvalue("__ht_stbc") or "0") + cfgs.HT_LDPC = mtkwifi.token_set(cfgs.HT_LDPC, vif_idx, http.formvalue("__ht_ldpc") or "0") + cfgs.VHT_STBC = mtkwifi.token_set(cfgs.VHT_STBC, vif_idx, http.formvalue("__vht_stbc") or "0") + cfgs.VHT_LDPC = mtkwifi.token_set(cfgs.VHT_LDPC, vif_idx, http.formvalue("__vht_ldpc") or "0") + cfgs.DLSCapable = mtkwifi.token_set(cfgs.DLSCapable, vif_idx, http.formvalue("__dls_capable") or "0") + cfgs.APSDCapable = mtkwifi.token_set(cfgs.APSDCapable, vif_idx, http.formvalue("__apsd_capable") or "0") + cfgs.FragThreshold = mtkwifi.token_set(cfgs.FragThreshold, vif_idx, http.formvalue("__frag_threshold") or "0") + cfgs.RTSThreshold = mtkwifi.token_set(cfgs.RTSThreshold, vif_idx, http.formvalue("__rts_threshold") or "0") + cfgs.VHT_SGI = mtkwifi.token_set(cfgs.VHT_SGI, vif_idx, http.formvalue("__vht_sgi") or "0") + cfgs.VHT_BW_SIGNAL = mtkwifi.token_set(cfgs.VHT_BW_SIGNAL, vif_idx, http.formvalue("__vht_bw_signal") or "0") + cfgs.HT_PROTECT = mtkwifi.token_set(cfgs.HT_PROTECT, vif_idx, http.formvalue("__ht_protect") or "0") + cfgs.HT_GI = mtkwifi.token_set(cfgs.HT_GI, vif_idx, http.formvalue("__ht_gi") or "0") + cfgs.HT_OpMode = mtkwifi.token_set(cfgs.HT_OpMode, vif_idx, http.formvalue("__ht_opmode") or "0") + cfgs.HT_AMSDU = mtkwifi.token_set(cfgs.HT_AMSDU, vif_idx, http.formvalue("__ht_amsdu") or "0") + cfgs.HT_AutoBA = mtkwifi.token_set(cfgs.HT_AutoBA, vif_idx, http.formvalue("__ht_autoba") or "0") + cfgs.IgmpSnEnable = mtkwifi.token_set(cfgs.IgmpSnEnable, vif_idx, http.formvalue("__igmp_snenable") or "0") + cfgs.WirelessMode = mtkwifi.token_set(cfgs.WirelessMode, vif_idx, http.formvalue("__wirelessmode") or "0") + cfgs.WdsEnable = mtkwifi.token_set(cfgs.WdsEnable, vif_idx, http.formvalue("__wdsenable") or "0") + cfgs.MuOfdmaDlEnable = mtkwifi.token_set(cfgs.MuOfdmaDlEnable, vif_idx, http.formvalue("__muofdma_dlenable") or "0") + cfgs.MuOfdmaUlEnable = mtkwifi.token_set(cfgs.MuOfdmaUlEnable, vif_idx, http.formvalue("__muofdma_ulenable") or "0") + cfgs.MuMimoDlEnable = mtkwifi.token_set(cfgs.MuMimoDlEnable, vif_idx, http.formvalue("__mumimo_dlenable") or "0") + cfgs.MuMimoUlEnable = mtkwifi.token_set(cfgs.MuMimoUlEnable, vif_idx, http.formvalue("__mumimo_ulenable") or "0") + +end + +function vif_cfg(dev, vif) + local devname, vifname = dev, vif + if not devname then devname = vif end + debug_write("devname="..devname) + debug_write("vifname="..(vifname or "")) + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + --local ssid_index; + --ssid_index = devs[devname]["vifs"][vifname].vifidx + + local cfgs = mtkwifi.load_profile(profile) + + for k,v in pairs(http.formvalue()) do + if type(v) == type("") or type(v) == type(0) then + nixio.syslog("debug", "post."..k.."="..tostring(v)) + else + nixio.syslog("debug", "post."..k.." invalid, type="..type(v)) + end + end + + -- sometimes vif_idx start from 0, like AccessPolicy0 + -- sometimes it starts from 1, like WPAPSK1. nice! + local vif_idx + local to_url + if http.formvalue("__action") == "vif_cfg_view" then + vif_idx = devs[devname]["vifs"][vifname].vifidx + debug_write("vif_idx=", vif_idx, devname, vifname) + to_url = luci.dispatcher.build_url("admin", "mtk", "wifi", "vif_cfg_view", devname, vifname) + elseif http.formvalue("__action") == "vif_add_view" then + cfgs.BssidNum = tonumber(cfgs.BssidNum) + 1 + vif_idx = tonumber(cfgs.BssidNum) + to_url = luci.dispatcher.build_url("admin", "mtk", "wifi") + -- initializing ; separated parameters for the new interface + cfgs = initialize_multiBssParameters(cfgs, vif_idx) + end + assert(vif_idx) + assert(to_url) + -- "__" should not be the prefix of a name if user wants to copy form value data directly to the dat file variable + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "vif_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + debug_write("vif_cfg: Copying",k,v) + cfgs[k] = v or "" + end + end + + -- WDS + -- Update WdsXKey if respective WdsEncrypType is NONE + for i=0,3 do + if (cfgs["Wds"..i.."Key"] and cfgs["Wds"..i.."Key"] ~= "") and + ((not mtkwifi.token_get(cfgs["WdsEncrypType"],i+1,nil)) or + ("NONE" == mtkwifi.token_get(cfgs["WdsEncrypType"],i+1,nil))) then + cfgs["Wds"..i.."Key"] = "" + end + end + + cfgs["AccessPolicy"..vif_idx-1] = http.formvalue("__accesspolicy") + local t = mtkwifi.parse_mac(http.formvalue("__maclist")) + cfgs["AccessControlList"..vif_idx-1] = table.concat(t, ";") + + __security_cfg(cfgs, vif_idx) + __update_mbss_para(cfgs, vif_idx) + __set_wifi_wpsconf(cfgs, http.formvalue("WPSRadio"), vif_idx) + + __mtkwifi_save_profile(cfgs, profile, false) + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",to_url)) + else + luci.http.redirect(to_url) + end +end + +function get_WPS_Info(devname, ifname) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + -- Create the applied settings backup file if it does not exist. + if not mtkwifi.exists(mtkwifi.__profile_applied_settings_path(profile)) then + os.execute("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + end + local applied_cfgs = mtkwifi.load_profile(mtkwifi.__profile_applied_settings_path(profile)) + + local WPS_details = {} + WPS_details = c_getCurrentWscProfile(ifname) + + if type(WPS_details) ~= "table" then + WPS_details["DRIVER_RSP"] = "NO" + else + WPS_details["DRIVER_RSP"] = "YES" + local isCfgsChanged = false -- To indicate that the settings have been changed by External Registrar. + local isBasicTabUpdateRequired = false + + if type(WPS_details["SSID"]) == "string" then + if applied_cfgs["SSID"..ssid_index] ~= WPS_details["SSID"] then + cfgs["SSID"..ssid_index] = WPS_details["SSID"] + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["SSID"] = cfgs["SSID"..ssid_index] + end + + if type(WPS_details["AuthMode"]) == "string" then + local auth_mode_ioctl = WPS_details["AuthMode"]:gsub("%W",""):upper() + local auth_mode_applied = mtkwifi.token_get(applied_cfgs.AuthMode, ssid_index, "") + if auth_mode_applied ~= auth_mode_ioctl then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, ssid_index, auth_mode_ioctl) + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["AuthMode"] = mtkwifi.token_get(cfgs.AuthMode, ssid_index, "") + end + + if type(WPS_details["EncType"]) == "string" then + local enc_type_ioctl = WPS_details["EncType"]:upper() + local enc_type_applied = mtkwifi.token_get(applied_cfgs.EncrypType, ssid_index, "") + if enc_type_applied ~= enc_type_ioctl then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, ssid_index, enc_type_ioctl) + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["EncType"] = mtkwifi.token_get(cfgs.EncrypType, ssid_index, "") + end + + if type(WPS_details["WscWPAKey"]) == "string" then + if applied_cfgs["WPAPSK"..ssid_index] ~= WPS_details["WscWPAKey"] then + cfgs["WPAPSK"..ssid_index] = WPS_details["WscWPAKey"] + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["WscWPAKey"] = cfgs["WPAPSK"..ssid_index] + end + + if type(WPS_details["DefKey"]) == "number" then + local def_key_applied = tonumber(mtkwifi.token_get(applied_cfgs.DefaultKeyID, ssid_index, "")) + if def_key_applied ~= WPS_details["DefKey"] then + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, ssid_index, WPS_details["DefKey"]) + isCfgsChanged = true + end + else + WPS_details["DefKey"] = tonumber(mtkwifi.token_get(cfgs.DefaultKeyID, ssid_index, 0)) or "" + end + + if type(WPS_details["Conf"]) == "number" then + local wsc_conf_status_applied = tonumber(mtkwifi.token_get(applied_cfgs.WscConfStatus, ssid_index, "")) + if wsc_conf_status_applied ~= WPS_details["Conf"] then + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, ssid_index, WPS_details["Conf"]) + isCfgsChanged = true + end + else + WPS_details["Conf"] = mtkwifi.token_get(cfgs.WscConfStatus, ssid_index, "") + end + + WPS_details["IS_BASIC_TAB_UPDATE_REQUIRED"] = isBasicTabUpdateRequired + + if isCfgsChanged then + -- Driver updates the *.dat file for following scenarios, + -- 1. When WPS Conf Status is not configured i.e. WscConfStatus is not set as 2, + -- and connection with a station is established i.e. where station acts as an External Registrar. + -- 2. When below settings are changed through External Registrar irrespective of WPS Conf Status + -- Update mtkwifi.__profile_applied_settings_path(profile) file with the + -- new settings to avoid display of "reload to apply changes" message. + applied_cfgs["WPAPSK"] = cfgs["WPAPSK"] + applied_cfgs["SSID"] = cfgs["SSID"] + applied_cfgs["SSID"..ssid_index] = cfgs["SSID"..ssid_index] + applied_cfgs["AuthMode"] = cfgs["AuthMode"] + applied_cfgs["EncrypType"] = cfgs["EncrypType"] + applied_cfgs["WPAPSK"..ssid_index] = cfgs["WPAPSK"..ssid_index] + applied_cfgs["DefaultKeyID"] = cfgs["DefaultKeyID"] + applied_cfgs["WscConfStatus"] = cfgs["WscConfStatus"] + mtkwifi.save_profile(applied_cfgs, mtkwifi.__profile_applied_settings_path(profile)) + end + end + http.write_json(WPS_details) +end + +function get_wifi_pin(ifname) + local pin = "" + pin = c_getApPin(ifname) + http.write_json(pin) +end + +function set_wifi_gen_pin(ifname,devname) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + os.execute("iwpriv "..ifname.." set WscGenPinCode") + + pin = c_getApPin(ifname) + cfgs["WscVendorPinCode"]=pin["genpincode"] + + --existing c code... done nothing for this segment as it read flash data and write to related .dat file. + -- no concept of nvram zones here + --if (nvram == RT2860_NVRAM) + -- do_system("ralink_init make_wireless_config rt2860"); + --else + -- do_system("ralink_init make_wireless_config rtdev"); + __mtkwifi_save_profile(cfgs, profile, true) + http.write_json(pin) +end + +function set_wifi_wps_oob(devname, ifname) + local SSID, mac = "" + local ssid_index = 0 + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + ssid_index = devs[devname]["vifs"][ifname].vifidx + mac = c_get_macaddr(ifname) + + if (mac["macaddr"] ~= "") then + SSID = "RalinkInitAP"..(ssid_index-1).."_"..mac["macaddr"] + else + SSID = "RalinkInitAP"..(ssid_index-1).."_unknown" + end + + cfgs["SSID"..ssid_index]=SSID + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, ssid_index, "1") + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, ssid_index, "WPA2PSK") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, ssid_index, "AES") + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, ssid_index, "2") + + cfgs["WPAPSK"..ssid_index]="12345678" + cfgs["WPAPSK"]="" + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, ssid_index, "0") + + os.execute("iwpriv "..ifname.." set SSID="..SSID ) + debug_write("iwpriv "..ifname.." set SSID="..SSID ) + os.execute("iwpriv "..ifname.." set AuthMode=WPA2PSK") + debug_write("iwpriv "..ifname.." set AuthMode=WPA2PSK") + os.execute("iwpriv "..ifname.." set EncrypType=AES") + debug_write("iwpriv "..ifname.." set EncrypType=AES") + os.execute("iwpriv "..ifname.." set WPAPSK=12345678") + debug_write("iwpriv "..ifname.." set WPAPSK=12345678") + os.execute("iwpriv "..ifname.." set SSID="..SSID) + debug_write("iwpriv "..ifname.." set SSID="..SSID) + + cfgs = mtkwifi.__restart_if_wps(devname, ifname, cfgs) + __mtkwifi_save_profile(cfgs, profile, true) + + mtkwifi.__run_in_child_env(__restart_all_daemons, devname, ifname) + + os.execute("iwpriv "..ifname.." set WscConfStatus=1") + debug_write("iwpriv "..ifname.." set WscConfStatus=1") + + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "vif_cfg_view", devname, ifname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) +end + +function set_wifi_do_wps(ifname, devname, wsc_pin_code_w) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + local wsc_mode = 0 + local wsc_conf_mode + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + if(wsc_pin_code_w == "nopin") then + wsc_mode=2 + else + wsc_mode=1 + end + + wsc_conf_mode = mtkwifi.token_get(cfgs["WscConfMode"], ssid_index, nil) + + if (wsc_conf_mode == 0) then + print("{\"wps_start\":\"WPS_NOT_ENABLED\"}") + DBG_MSG("WPS is not enabled before do PBC/PIN.\n") + return + end + + if (wsc_mode == 1) then + __wps_ap_pin_start_all(ifname, wsc_pin_code_w) + + elseif (wsc_mode == 2) then + __wps_ap_pbc_start_all(ifname) + else + http.write_json("{\"wps_start\":\"NG\"}") + return + end + cfgs["WscStartIF"] = ifname + + -- execute wps_action.lua file to send signal for current interface + os.execute("lua wps_action.lua "..ifname) + + http.write_json("{\"wps_start\":\"OK\"}") +end + +function get_wps_security(ifname, devname) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + assert(profile) + local output = {} + local cfgs = mtkwifi.load_profile(profile) + + output["AuthMode"] = mtkwifi.token_get(cfgs.AuthMode,ssid_index) + output["IEEE8021X"] = mtkwifi.token_get(cfgs.IEEE8021X,ssid_index) + + http.write_json(output) +end + +function apcli_get_wps_status(ifname, devname) + local output = {} + local ssid_index = 0 + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + -- apcli interface has a different structure as compared to other vifs + ssid_index = devs[devname][ifname].vifidx + output = c_apcli_get_wps_status(ifname) + if (output.wps_port_secured == "YES") then + local cfgs = mtkwifi.load_profile(profile) + cfgs.ApCliSsid = mtkwifi.token_set(cfgs.ApCliSsid, ssid_index, output.enr_SSID) + cfgs.ApCliEnable = mtkwifi.token_set(cfgs.ApCliEnable, ssid_index, "1") + cfgs.ApCliAuthMode = mtkwifi.token_set(cfgs.ApCliAuthMode, ssid_index, output.enr_AuthMode) + cfgs.ApCliEncrypType = mtkwifi.token_set(cfgs.ApCliEncrypType, ssid_index, output.enr_EncrypType) + cfgs.ApCliDefaultKeyID = mtkwifi.token_set(cfgs.ApCliDefaultKeyID, ssid_index, output.enr_DefaultKeyID) + cfgs.Channel = mtkwifi.read_pipe("iwconfig "..ifname.." | grep Channel | cut -d = -f 2 | cut -d \" \" -f 1") + debug_write("iwconfig "..ifname.." | grep Channel | cut -d = -f 2 | cut -d \" \" -f 1") + + if(output.enr_EncrypType == "WEP") then + for i = 1, 4 do + cfgs["ApCliKey"..i.."Type"] = mtkwifi.token_set(cfgs["ApCliKey"..i.."Type"], ssid_index, output["Key"..i.."Type"]) + end + if(ssid_index == "0") then + cfgs["ApCliKey"..output.enr_DefaultKeyID.."Str"] = output.enr_KeyStr + else + cfgs["ApCliKey"..output.enr_DefaultKeyID.."Str"..ssid_index] = output.enr_KeyStr + end + elseif(output.enr_EncrypType == "TKIP") or (output.enr_EncrypType == "AES") or (output.enr_EncrypType == "TKIPAES") then + if(output.enr_AuthMode ~= "WPAPSKWPA2PSK") then + cfgs["ApCliWPAPSK"] = output.enr_WPAPSK + end + end + __mtkwifi_save_profile(cfgs, profile, true) + end + http.write_json(output); +end + +function string.tohex(str) + return (str:gsub('.', function (c) + return string.format('%02X', string.byte(c)) + end)) +end + +function unencode_ssid(raw_ssid) + local c + local output = "" + local convertNext = 0 + for c in raw_ssid:gmatch"." do + if(convertNext == 0) then + if(c == '+') then + output = output..' ' + elseif(c == '%') then + convertNext = 1 + else + output = output..c + end + else + output = output..string.tohex(c) + convertNext = 0 + end + end + return output +end + +function decode_ssid(raw_ssid) + local output = raw_ssid + output = output:gsub("&", "&") + output = output:gsub("<", "<") + output = output:gsub(">", ">") + output = output:gsub(""", "\"") + output = output:gsub("'", "'") + output = output:gsub(" ", " ") + for codenum in raw_ssid:gmatch("&#(%d+);") do + output = output:gsub("&#"..codenum..";", string.char(tonumber(codenum))) + end + return output +end + +function apcli_do_enr_pin_wps(ifname, devname, raw_ssid) + local target_ap_ssid = "" + local ret_value = {} + if(raw_ssid == "") then + ret_value["apcli_do_enr_pin_wps"] = "GET_SSID_NG" + end + ret_value["raw_ssid"] = raw_ssid + target_ap_ssid = decode_ssid(raw_ssid) + target_ap_ssid = ''..mtkwifi.__handleSpecialChars(target_ap_ssid) + ret_value["target_ap_ssid"] = target_ap_ssid + if(target_ap_ssid == "") then + ret_value["apcli_do_enr_pin_wps"] = "GET_SSID_NG" + else + ret_value["apcli_do_enr_pin_wps"] = "OK" + end + os.execute("ifconfig "..ifname.." up") + debug_write("ifconfig "..ifname.." up") + os.execute("brctl addif br0 "..ifname) + debug_write("brctl addif br0 "..ifname) + os.execute("brctl addif br-lan "..ifname) + debug_write("brctl addif br-lan "..ifname) + os.execute("iwpriv "..ifname.." set ApCliAutoConnect=1") + os.execute("iwpriv "..ifname.." set ApCliEnable=1") + debug_write("iwpriv "..ifname.." set ApCliEnable=1") + --os.execute("iwpriv "..ifname.." set WscConfMode=0") + os.execute("iwpriv "..ifname.." set WscConfMode=1") + debug_write("iwpriv "..ifname.." set WscConfMode=1") + os.execute("iwpriv "..ifname.." set WscMode=1") + debug_write("iwpriv "..ifname.." set WscMode=1") + os.execute("iwpriv "..ifname.." set ApCliWscSsid=\""..target_ap_ssid.."\"") + debug_write("iwpriv "..ifname.." set ApCliWscSsid=\""..target_ap_ssid.."\"") + os.execute("iwpriv "..ifname.." set WscGetConf=1") + debug_write("iwpriv "..ifname.." set WscGetConf=1") + -- check interface value to correlate with nvram as values will be like apclixxx + os.execute("wps_action.lua "..ifname) + http.write_json(ret_value) +end + +function apcli_do_enr_pbc_wps(ifname, devname) + local ret_value = {} + + --os.execute("iwpriv "..ifname.." set ApCliAutoConnect=1") + --os.execute("iwpriv "..ifname.." set ApCliEnable=1") + --os.execute("ifconfig "..ifname.." up") + --os.execute("brctl addif br0 "..ifname) + --os.execute("iwpriv "..ifname.." set WscConfMode=0") + os.execute("iwpriv "..ifname.." set WscConfMode=1") + os.execute("iwpriv "..ifname.." set WscMode=2") + os.execute("iwpriv "..ifname.." set WscGetConf=1") + -- check interface value to correlate with nvram as values will be like apclixxx + os.execute("wps_action.lua "..ifname) + + --debug_write("iwpriv "..ifname.." set ApCliEnable=1") + --debug_write("brctl addif br0 "..ifname) + --debug_write("ifconfig "..ifname.." up") + debug_write("iwpriv "..ifname.." set WscConfMode=1") + debug_write("iwpriv "..ifname.." set WscMode=2") + debug_write("iwpriv "..ifname.." set WscGetConf=1") + ret_value["apcli_do_enr_pbc_wps"] = "OK" + http.write_json(ret_value) +end + +function apcli_cancel_wps(ifname) + local ret_value = {} + os.execute("iwpriv "..ifname.." set WscStop=1") + os.execute("miniupnpd.sh init") + -- check interface value to correlate with nvram as values will be like apclixxx + os.execute("wps_action.lua "..ifname) + ret_value["apcli_cancel_wps"] = "OK" + http.write_json(ret_value) +end + +function apcli_wps_gen_pincode(ifname) + local ret_value = {} + os.execute("iwpriv "..ifname.." set WscGenPinCode") + ret_value["apcli_wps_gen_pincode"] = "OK" + http.write_json(ret_value) +end + +function apcli_wps_get_pincode(ifname) + local output = c_apcli_wps_get_pincode(ifname) + http.write_json(output) +end + +function get_apcli_conn_info(ifname) + local rsp = {} + if not ifname then + rsp["conn_state"]="Disconnected" + else + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..ifname.."/flags 2>/dev/null")) or 0 + rsp["infc_state"] = flags%2 == 1 and "up" or "down" + local iwapcli = mtkwifi.read_pipe("iwconfig "..ifname.." | grep ESSID 2>/dev/null") + local ssid = string.match(iwapcli, "ESSID:\"(.*)\"") + iwapcli = mtkwifi.read_pipe("iwconfig "..ifname.." | grep 'Access Point' 2>/dev/null") + local bssid = string.match(iwapcli, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") + if not ssid or ssid == "" then + rsp["conn_state"]= "Disconnected" + else + rsp["conn_state"] = "Connected" + rsp["ssid"] = ssid + rsp["bssid"] = bssid or "N/A" + end + end + http.write_json(rsp) +end + +function sta_info(ifname) + local output = {} + local stalist = c_StaInfo(ifname) + + local count = 0 + for _ in pairs(stalist) do count = count + 1 end + + for i=0, count - 1 do + table.insert(output, stalist[i]) + end + http.write_json(output) +end + +function apcli_scan(ifname) + local aplist = mtkwifi.scan_ap(ifname) + local convert=""; + for i=1, #aplist do + convert = c_convert_string_display(aplist[i]["ssid"]) + aplist[i]["original_ssid"] = aplist[i]["ssid"] + aplist[i]["ssid"] = convert["output"] + end + http.write_json(aplist) +end + +function get_station_list() + http.write("get_station_list") +end + +function reset_wifi(devname) + if devname then + os.execute("cp -f /rom/etc/wireless/"..devname.."/ /etc/wireless/") + else + os.execute("cp -rf /rom/etc/wireless /etc/") + end + return luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function reload_wifi(devname) + profiles = mtkwifi.search_dev_and_profile() + path = profiles[devname] + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) +end + +function get_raw_profile() + local sid = http.formvalue("sid") + http.write_json("get_raw_profile") +end + +function get_country_region_list() + local mode = http.formvalue("mode") + local cr_list; + + if mtkwifi.band(mode) == "5G" then + cr_list = mtkwifi.CountryRegionList_5G_All + elseif mtkwifi.band(mode) == "6G" then + cr_list = mtkwifi.CountryRegionList_6G_All + else + cr_list = mtkwifi.CountryRegionList_2G_All + end + + http.write_json(cr_list) +end + +function remove_ch_by_region(ch_list, region) + for i = #ch_list,2,-1 do + if not ch_list[i].region[region] then + table.remove(ch_list, i) + end + end +end + +function get_channel_list() + local mode = http.formvalue("mode") + local region = tonumber(http.formvalue("country_region")) or 1 + local ch_list + + if mtkwifi.band(mode) == "5G" then + ch_list = mtkwifi.ChannelList_5G_All + elseif mtkwifi.band(mode) == "6G" then + ch_list = mtkwifi.ChannelList_6G_All + else + ch_list = mtkwifi.ChannelList_2G_All + end + + remove_ch_by_region(ch_list, region) + http.write_json(ch_list) +end + +function get_HT_ext_channel_list() + local mode = http.formvalue("mode") + local ch_cur = tonumber(http.formvalue("ch_cur")) + local region = tonumber(http.formvalue("country_region")) or 1 + local ext_ch_list = {} + + if mtkwifi.band(mode) == "6G" then -- 6G Channel + local ch_list = mtkwifi.ChannelList_6G_All + local ext_ch_idx = -1 + local len = 0 + + for k, v in ipairs(ch_list) do + len = len + 1 + if v.channel == ch_cur then + ext_ch_idx = (k % 2 == 0) and k + 1 or k - 1 + end + end + + if ext_ch_idx > 0 and ext_ch_idx < len and ch_list[ext_ch_idx].region[region] then + ext_ch_list[1] = {} + ext_ch_list[1].val = ext_ch_idx % 2 + ext_ch_list[1].text = ch_list[ext_ch_idx].text + end + + elseif mtkwifi.band(mode) == "2.4G" then -- 2.4G Channel + local ch_list = mtkwifi.ChannelList_2G_All + local below_ch = ch_cur - 4 + local above_ch = ch_cur + 4 + local i = 1 + + if below_ch > 0 and ch_list[below_ch + 1].region[region] then + ext_ch_list[i] = {} + ext_ch_list[i].val = 0 + ext_ch_list[i].text = ch_list[below_ch + 1].text + i = i + 1 + end + + if above_ch <= 14 and ch_list[above_ch + 1].region[region] then + ext_ch_list[i] = {} + ext_ch_list[i].val = 1 + ext_ch_list[i].text = ch_list[above_ch + 1].text + end + else -- 5G Channel + local ch_list = mtkwifi.ChannelList_5G_All + local ext_ch_idx = -1 + local len = 0 + + for k, v in ipairs(ch_list) do + len = len + 1 + if v.channel == ch_cur then + ext_ch_idx = (k % 2 == 0) and k + 1 or k - 1 + end + end + + if ext_ch_idx > 0 and ext_ch_idx < len and ch_list[ext_ch_idx].region[region] then + ext_ch_list[1] = {} + ext_ch_list[1].val = ext_ch_idx % 2 + ext_ch_list[1].text = ch_list[ext_ch_idx].text + end + end + + http.write_json(ext_ch_list) +end + +function get_5G_2nd_80Mhz_channel_list() + local ch_cur = tonumber(http.formvalue("ch_cur")) + local region = tonumber(http.formvalue("country_region")) + local ch_list = mtkwifi.ChannelList_5G_2nd_80MHZ_ALL + local ch_list_5g = mtkwifi.ChannelList_5G_All + local i, j, test_ch, test_idx + local bw80_1st_idx = -1 + + -- remove adjacent freqencies starting from list tail. + for i = #ch_list,1,-1 do + for j = 0,3 do + if ch_list[i].channel == -1 then + break + end + + test_ch = ch_list[i].channel + j * 4 + test_idx = ch_list[i].chidx + j + + if test_ch == ch_cur then + if i + 1 <= #ch_list and ch_list[i + 1] then + table.remove(ch_list, i + 1) + end + table.remove(ch_list, i) + bw80_1st_idx = i + break + end + + if i == (bw80_1st_idx - 1) or (not ch_list_5g[test_idx].region[region]) then + table.remove(ch_list, i) + break + end + end + end + + -- remove unused channel. + for i = #ch_list,1,-1 do + if ch_list[i].channel == -1 then + table.remove(ch_list, i) + end + end + http.write_json(ch_list) +end + +function webcmd() + local cmd = http.formvalue("cmd") + if cmd then + local result = mtkwifi.read_pipe(tostring(cmd).." 2>&1") + result = result:gsub("<", "<") + http.write(tostring(result)) + else + http.write_json(http.formvalue()) + end +end + +function net_cfg() + http.write_json(http.formvalue()) +end + +function apcli_cfg(dev, vif) + local devname = dev + debug_write(devname) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + + local cfgs = mtkwifi.load_profile(profiles[devname]) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "apcli_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + + if cfgs['ApCliEnable'] == '1' then + os.execute("brctl addif br-lan "..vif) + end + + -- http.write_json(http.formvalue()) + + -- Mediatek Adaptive Network + --[=[ moved to a separated page + if cfgs.ApCliEzEnable then + cfgs.EzEnable = cfgs.ApCliEzEnable + cfgs.ApMWDS = cfgs.ApCliMWDS + cfgs.EzConfStatus = cfgs.ApCliEzConfStatus + cfgs.EzOpenGroupID = cfgs.ApCliEzOpenGroupID + if http.formvalue("__group_id_mode") == "0" then + cfgs.EzGroupID = cfgs.ApCliEzGroupID + cfgs.EzGenGroupID = "" + cfgs.ApCliEzGenGroupID = "" + else + cfgs.EzGroupID = "" + cfgs.ApCliEzGroupID = "" + cfgs.EzGenGroupID = cfgs.ApCliEzGenGroupID + end + -- if dbdc + -- os.execute("app_ez &") + -- os.execute("ManDaemon ") + end + ]=] + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + -- M.A.N Push parameters + -- They are not part of wifi profile, we save it into /etc/man.conf. + + --[=[ moved to a separated page + local man_ssid = http.formvalue("__man_ssid_"..vifname) + local man_pass = http.formvalue("__man_pass_"..vifname) + local man_auth = http.formvalue("__man_auth_"..vifname) or "" + + if man_ssid and man_pass then + local fp = io.open("/etc/man."..vifname..".conf", "w+") + fp:write("__man_ssid_"..vifname.."="..man_ssid.."\n") + fp:write("__man_pass_"..vifname.."="..man_pass.."\n") + fp:write("__man_auth_"..vifname.."="..man_auth.."\n") + fp:close() + end + ]=] + + -- commented, do not connect by default + --[=[ + os.execute("iwpriv apcli0 set ApCliEnable=0") + os.execute("iwpriv apcli0 set Channel="..cfgs.Channel) + os.execute("iwpriv apcli0 set ApCliAuthMode="..cfgs.ApCliAuthMode) + os.execute("iwpriv apcli0 set ApCliEncrypType="..cfgs.ApCliEncrypType) + if cfgs.ApCliAuthMode == "WEP" then + os.execute("#iwpriv apcli0 set ApCliDefaultKeyID="..cfgs.ApCliDefaultKeyID) + os.execute("#iwpriv apcli0 set ApCliKey1="..cfgs.ApCliKey1Str) + elseif cfgs.ApCliAuthMode == "WPAPSK" + or cfgs.ApCliAuthMode == "WPA2PSK" + or cfgs.ApCliAuthMode == "WPAPSKWPA2PSK" then + os.execute("iwpriv apcli0 set ApCliWPAPSK="..cfgs.ApCliWPAPSK) + end + -- os.execute("iwpriv apcli0 set ApCliWirelessMode=") + os.execute("iwpriv apcli0 set ApCliSsid="..cfgs.ApCliSsid) + os.execute("iwpriv apcli0 set ApCliEnable=1") + ]=] + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "apcli_cfg_view", dev, vif) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "apcli_cfg_view", dev, vif)) + end +end + +function apcli_connect(dev, vif) + -- dev_vif can be + -- 1. mt7620.apcli0 # simple case + -- 2. mt7615e.1.apclix0 # multi-card + -- 3. mt7615e.1.2G.apclix0 # multi-card & multi-profile + local devname,vifname = dev, vif + debug_write("devname=", dev, "vifname=", vif) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profiles[devname], true) + os.execute("ifconfig "..vifname.." up") + os.execute("brctl addif br-lan "..vifname) + os.execute("iwpriv "..vifname.." set MACRepeaterEn="..cfgs.MACRepeaterEn) + os.execute("iwpriv "..vifname.." set ApCliEnable=0") + os.execute("iwpriv "..vifname.." set Channel="..cfgs.Channel) + os.execute("iwpriv "..vifname.." set ApCliAuthMode="..cfgs.ApCliAuthMode) + os.execute("iwpriv "..vifname.." set ApCliEncrypType="..cfgs.ApCliEncrypType) + if cfgs.ApCliEncrypType == "WEP" then + os.execute("iwpriv "..vifname.." set ApCliDefaultKeyID="..cfgs.ApCliDefaultKeyID) + if (cfgs.ApCliDefaultKeyID == "1") then + os.execute("iwpriv "..vifname.." set ApCliKey1=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey1Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "2") then + os.execute("iwpriv "..vifname.." set ApCliKey2=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey2Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "3") then + os.execute("iwpriv "..vifname.." set ApCliKey3=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey3Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "4") then + os.execute("iwpriv "..vifname.." set ApCliKey4=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey4Str).."\"") + end + elseif cfgs.ApCliAuthMode == "WPAPSK" + or cfgs.ApCliAuthMode == "WPA2PSK" + or cfgs.ApCliAuthMode == "WPAPSKWPA2PSK" then + os.execute("iwpriv "..vifname.." set ApCliWPAPSK=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliWPAPSK).."\"") + end + os.execute("iwpriv "..vifname.." set ApCliSsid=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliSsid).."\"") + os.execute("iwpriv "..vifname.." set ApCliEnable=1") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function apcli_disconnect(dev, vif) + -- dev_vif can be + -- 1. mt7620.apcli0 # simple case + -- 2. mt7615e.1.apclix0 # multi-card + -- 3. mt7615e.1.2G.apclix0 # multi-card & multi-profile + local devname,vifname = dev, vif + debug_write("devname=", dev, "vifname", vif) + debug_write(devname) + debug_write(vifname) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profiles[devname], true) + os.execute("iwpriv "..vifname.." set ApCliEnable=0") + os.execute("ifconfig "..vifname.." down") + os.execute("brctl delif br-lan "..vifname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +-- Mediatek Adaptive Network +function man_cfg() + local mtkwifi = require("mtkwifi") + local profiles = mtkwifi.search_dev_and_profile() + + for k,v in pairs(http.formvalue()) do + debug_write(k.."="..v) + end + + + for dev,profile in pairs(profiles) do + debug_write(dev.."=2======="..profile) + local cfgs = mtkwifi.load_profile(profile) + + if cfgs.ApCliEzEnable then + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "man_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + + debug_write(tostring(http.formvalue("__"..dev.."_ezsetup"))) + cfgs.ApCliEzEnable = http.formvalue("__"..dev.."_ezsetup") or "0" + + -- Yes this is bad. LSDK insists on this. + if cfgs.ApCliEzEnable == "1" then + cfgs.ApCliEnable = "1" + cfgs.ApCliMWDS = "1" + cfgs.ApCliAuthMode = "WPS2PSK" + cfgs.ApCliEncrypType = AES + cfgs.ApCliWPAPSK = "12345678" + cfgs.AuthMode = "WPA2PSK" + cfgs.EncrypType = "AES" + cfgs.RekeyMethod = "TIME" + cfgs.WPAPSK1 = "" + cfgs.RegroupSupport = "1;1" + end + + if http.formvalue("__group_id_mode") == "0" then + cfgs.EzGroupID = cfgs.ApCliEzGroupID + cfgs.EzGenGroupID = "" + cfgs.ApCliEzGenGroupID = "" + else + cfgs.EzGroupID = "" + cfgs.ApCliEzGroupID = "" + cfgs.EzGenGroupID = cfgs.ApCliEzGenGroupID + end + + cfgs.EzEnable = cfgs.ApCliEzEnable + cfgs.ApMWDS = cfgs.ApCliMWDS + cfgs.EzConfStatus = cfgs.ApCliEzConfStatus + cfgs.EzOpenGroupID = cfgs.ApCliEzOpenGroupID + end + __mtkwifi_save_profile(cfgs, profile, false) + end + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "man") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "man")) + end +end + +function apply_power_boost_settings() + local devname = http.formvalue("__devname") + local ret_status = {} + local devs = mtkwifi.get_all_devs() + local dev = {} + for _,v in ipairs(devs) do + if v.devname == devname then + dev = v + break + end + end + if next(dev) == nil then + ret_status["status"]= "Device "..(devname or "").." not found!" + elseif not dev.isPowerBoostSupported then + ret_status["status"]= "Power Boost feature is not supported by "..(devname or "").." Device!" + else + local cfgs = mtkwifi.load_profile(dev.profile) + if type(cfgs) ~= "table" or next(cfgs) == nil then + ret_status["status"]= "Profile settings file not found!" + else + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("ERROR: [apply_power_boost_settings] String expected; Got"..type(v).."for"..k.."key") + ret_status["status"]= "Power Boost settings are of incorrect type!" + break + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + if next(ret_status) == nil then + if type(dev.vifs) ~= "table" or next(dev.vifs) == nil or not cfgs.BssidNum or cfgs.BssidNum == "0" then + ret_status["status"]= "No Wireless Interfaces has been added yet!" + elseif cfgs.PowerUpenable ~= "1" then + ret_status["status"]= "Power Boost feature is not enabled!" + else + local up_vif_name_list = {} + for idx,vif in ipairs(dev.vifs) do + if vif.state == "up" and vif.vifname ~= nil and vif.vifname ~= "" and type(vif.vifname) == "string" then + up_vif_name_list[idx] = vif.vifname + end + end + if next(up_vif_name_list) == nil then + ret_status["status"]= "No Wireless Interfaces is up!" + else + for _,vifname in ipairs(up_vif_name_list) do + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=0:"..cfgs.PowerUpCckOfdm) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=1:"..cfgs.PowerUpHT20) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=2:"..cfgs.PowerUpHT40) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=3:"..cfgs.PowerUpVHT20) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=4:"..cfgs.PowerUpVHT40) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=5:"..cfgs.PowerUpVHT80) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=6:"..cfgs.PowerUpVHT160) + os.execute("sleep 1") -- Wait for 1 second to let driver process the above data + end + __mtkwifi_save_profile(cfgs, dev.profile, true) + ret_status["status"]= "SUCCESS" + end + end + end + end + end + http.write_json(ret_status) +end + +function get_bssid_num(devName) + local ret_status = {} + local profiles = mtkwifi.search_dev_and_profile() + for dev,profile in pairs(profiles) do + if devName == dev then + local cfgs = mtkwifi.load_profile(profile) + if type(cfgs) ~= "table" or next(cfgs) == nil then + ret_status["status"]= "Profile settings file not found!" + else + ret_status["status"] = "SUCCESS" + ret_status["bssidNum"] = cfgs.BssidNum + end + break + end + end + if next(ret_status) == nil then + ret_status["status"]= "Device "..(devName or "").." not found!" + end + http.write_json(ret_status) +end + +local exec_reset_to_defaults_cmd = function (devname) + if devname then + os.execute("wifi reset "..devname) + else + os.execute("wifi reset") + end +end + +function reset_to_defaults(devname) + mtkwifi.__run_in_child_env(exec_reset_to_defaults_cmd, devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",mtkwifi.get_referer_url())) +end + +local exec_reset_to_default_easymesh_cmd = function () + -- OpenWRT + if mtkwifi.exists("/usr/bin/EasyMesh_openwrt.sh") then + os.execute("/usr/bin/EasyMesh_openwrt.sh default") + elseif mtkwifi.exists("/usr/bin/EasyMesh_7622.sh") then + os.execute("/usr/bin/EasyMesh_7622.sh default") + elseif mtkwifi.exists("/usr/bin/EasyMesh_7629.sh") then + os.execute("/usr/bin/EasyMesh_7629.sh default") + end + -- LSDK + if mtkwifi.exists("/sbin/EasyMesh.sh") then + os.execute("EasyMesh.sh default") + end +end + +function reset_to_default_easymesh() + mtkwifi.__run_in_child_env(exec_reset_to_default_easymesh_cmd) + + if mtkwifi.exists("/etc/dpp_cfg.txt") then + local dpp_cfg = mtkwifi.load_profile("/etc/dpp_cfg.txt") + dpp_cfg.allowed_role = "1" + mtkwifi.save_profile(dpp_cfg, "/etc/dpp_cfg.txt") + end + + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",mtkwifi.get_referer_url())) +end + +function save_easymesh_driver_profile(easymesh_cfgs) + local profiles = mtkwifi.search_dev_and_profile() + local detected_5g = false + -- Following EasyMesh settings must be written to all DAT files of Driver, + -- 1. MapEnable + -- 2. MAP_Turnkey + for _,profile in mtkwifi.__spairs(profiles, function(a,b) return string.upper(a) < string.upper(b) end) do + local driver_cfgs = mtkwifi.load_profile(profile) + driver_cfgs['MapMode'] = easymesh_cfgs['MapMode'] + if http.formvalue("TriBand") == "1" then + if detected_5g == false and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "0:0:1:1" + detected_5g = true + elseif detected_5g == true and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "1:1:0:0" + end + elseif http.formvalue("TriBand") == "2" then + if detected_5g == false and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "1:1:0:0" + detected_5g = true + elseif detected_5g == true and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "0:0:1:1" + end + end + if driver_cfgs['MapMode'] == "1" then + driver_cfgs['SREnable'] = "0" + driver_cfgs['SRMode'] = "0" + end + if easymesh_cfgs['MeshSREnable'] == "1" then + driver_cfgs['SREnable'] = "1" + driver_cfgs['SRMode'] = "1" + driver_cfgs['MapBalance'] = "1" + driver_cfgs['BSSColorValue'] = "254" + elseif easymesh_cfgs['MeshSREnable'] == "0" then + driver_cfgs['SREnable'] = "0" + driver_cfgs['SRMode'] = "0" + driver_cfgs['MapBalance'] = "0" + driver_cfgs['BSSColorValue'] = "255" + end + __mtkwifi_save_profile(driver_cfgs, profile, false) + end +end + +function map_cfg() + local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__write_easymesh_profile_path()) + assert(easymesh_cfgs) + + local easymesh_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__write_easymesh_profile_path()) + os.execute("cp -f "..mtkwifi.__write_easymesh_profile_path().." "..easymesh_applied_path) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("map_cfg: Invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + debug_write("map_cfg: Copying key:"..k..","..type(v)) + easymesh_cfgs[k] = v or "" + end + end + + local bands = mtkwifi.detect_triband() + if bands ~= 3 then + easymesh_cfgs['BhPriority5GH'] = easymesh_cfgs['BhPriority5GL'] + end + + save_easymesh_driver_profile(easymesh_cfgs) + mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + + if http.formvalue("__apply") then + + if http.formvalue("__ChangeDeviceRole")=="changed" then + os.execute("wappctrl ra0 dpp dpp_reset_dpp_config_file") + end + + if mtkwifi.exists("/etc/dpp_cfg.txt") then + local dpp_cfg = mtkwifi.load_profile("/etc/dpp_cfg.txt") + if http.formvalue("DeviceRole")=="1" then + dpp_cfg.allowed_role="2" + elseif http.formvalue("DeviceRole")== "2" then + dpp_cfg.allowed_role="1" + elseif http.formvalue("DeviceRole")== "0" then + dpp_cfg.allowed_role="0" + end + mtkwifi.save_profile(dpp_cfg, "/etc/dpp_cfg.txt") + end + + if mtkwifi.exists("/usr/bin/map_restart.sh") then + mtkwifi.__run_in_child_env(exec_map_restart) + else + mtkwifi.__run_in_child_env(__mtkwifi_reload) + end + + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "multi_ap") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) + end +end + +function exec_map_restart() + if mtkwifi.exists("/usr/bin/map_restart.sh") then + os.execute("/usr/bin/map_restart.sh") + end +end + +function get_device_role() + local devRole = c_get_device_role() + -- Set ApCliEnable as "1" for Device with on-boarded ApCli interface to let + -- UI display connection information of ApCli interface on Wireless Overview web-page. + if tonumber(devRole.mapDevRole) == 2 then + local r = mtkwifi.get_easymesh_on_boarded_iface_info() + if r['status'] == "SUCCESS" then + for profile in string.gmatch(r['profile'],'(.-%.dat);') do + local cfgs = mtkwifi.load_profile(profile) + if cfgs.ApCliEnable ~= "1" or cfgs.ApCliEnable == nil then + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profile, true) + end + end + end + end + http.write_json(devRole) +end + +function trigger_uplink_ap_selection() + local r = c_trigger_uplink_ap_selection() + http.write_json(r) +end + +function trigger_mandate_steering_on_agent(sta_mac, target_bssid) + sta_mac = sta_mac:sub(1,17) + target_bssid = target_bssid:sub(1,17) + local r = c_trigger_mandate_steering_on_agent(sta_mac, target_bssid) + http.write_json(r) +end + +function trigger_back_haul_steering_on_agent(bh_mac, bh_target_bssid) + bh_mac = bh_mac:sub(1,17) + bh_target_bssid = bh_target_bssid:sub(1,17) + local r = c_trigger_back_haul_steering_on_agent(bh_mac, bh_target_bssid) + http.write_json(r) +end + +function trigger_wps_fh_agent(fh_bss_mac) + fh_bss_mac = fh_bss_mac:sub(1,17) + local r = c_trigger_wps_fh_agent(fh_bss_mac) + http.write_json(r) +end + +function trigger_multi_ap_on_boarding(ifmed) + assert(ifmed) + onboardingType = ifmed + debug_write("trigger_multi_ap_on_boarding: onboardingType:"..ifmed) + local r = c_trigger_multi_ap_on_boarding(ifmed) + http.write_json(r) +end + +function get_runtime_topology() + local r = c_get_runtime_topology() + http.write_json(r) +end + +function get_client_capabilities() + local r = c_get_client_capabilities() + http.write_json(r) +end + +function get_bh_connection_status() + local r = c_get_bh_connection_status() + http.write_json(r) +end + +function get_sta_steering_progress() + local r = {} + local fd = io.open("/tmp/sta_steer_progress","r") + if not fd then + r["status"] = "Failed to open /tmp/sta_steer_progress file in read mode!" + else + r["sta_steering_info"] = fd:read("*all") + fd:close() + r["status"] = "SUCCESS" + end + http.write_json(r) +end + +function get_al_mac(devRole) + local r = mtkwifi.get_easymesh_al_mac(devRole) + http.write_json(r) +end + +function apply_wifi_bh_priority(bhPriority2G, bhPriority5GL, bhPriority5GH) + assert(bhPriority2G) + assert(bhPriority5GL) + assert(bhPriority5GH) + debug_write("apply_wifi_bh_priority:BhPriority2G:"..bhPriority2G..", BhPriority5GL: "..bhPriority5GL..", BhPriority5GH: "..bhPriority5GH) + local r = c_apply_wifi_bh_priority(bhPriority2G, bhPriority5GL, bhPriority5GH) + if r.status == "SUCCESS" then + local read_easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + read_easymesh_cfgs['BhPriority2G'] = bhPriority2G + read_easymesh_cfgs['BhPriority5GL'] = bhPriority5GL + read_easymesh_cfgs['BhPriority5GH'] = bhPriority5GH + mtkwifi.save_read_easymesh_profile(read_easymesh_cfgs) + + local write_easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__write_easymesh_profile_path()) + write_easymesh_cfgs['BhPriority2G'] = bhPriority2G + write_easymesh_cfgs['BhPriority5GL'] = bhPriority5GL + write_easymesh_cfgs['BhPriority5GH'] = bhPriority5GH + mtkwifi.save_write_easymesh_profile(write_easymesh_cfgs) + end + http.write_json(r) +end + +function apply_ap_steer_rssi_th(rssi) + assert(rssi) + local r = c_apply_ap_steer_rssi_th(rssi) + if r.status == "SUCCESS" then + local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + if easymesh_cfgs['APSteerRssiTh'] ~= rssi then + easymesh_cfgs['APSteerRssiTh'] = rssi + mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + end + local easymesh_mapd_cfgs = mtkwifi.load_profile(mtkwifi.__easymesh_mapd_profile_path()) + local mapd_rssi = tonumber(rssi) + 94 + if easymesh_mapd_cfgs['LowRSSIAPSteerEdge_RE'] ~= mapd_rssi then + easymesh_mapd_cfgs['LowRSSIAPSteerEdge_RE'] = mapd_rssi + mtkwifi.save_easymesh_mapd_profile(easymesh_mapd_cfgs) + end + end + http.write_json(r) +end + +function apply_force_ch_switch(agent_almac, channel1, channel2, channel3) + agent_almac = agent_almac:sub(1,17) + + if channel1 == nil then + channel1 = "" + end + + if channel2 == nil then + channel2 = "" + end + + if channel3 == nil then + channel3 = "" + end + + debug_write("apply_force_ch_switch() enter, agent_almac: "..agent_almac..", channel1:"..channel1..", channel2:"..channel2..", channe3:"..channel3) + local r = c_apply_force_ch_switch(agent_almac, channel1, channel2, channel3) + debug_write("apply_force_ch_switch() status: "..r.status) + http.write_json(r) +end + +function apply_user_preferred_channel(channel) + assert(channel) + debug_write("apply_user_preferred_channel() enter, channel:"..channel) + local r = c_apply_user_preferred_channel(channel) + debug_write("apply_user_preferred_channel() status: "..r.status) + http.write_json(r) +end + +function trigger_channel_planning_r2(band) + assert(band) + local r = c_trigger_channel_planning_r2(band) + http.write_json(r) +end + +function trigger_de_dump(almac) + assert(almac) + local r = c_trigger_de_dump(almac) + http.write_json(r) +end + +function get_data_element() + local r = c_get_data_element() + http.write_json(r) +end + +function trigger_channel_scan(almac) + assert(almac) + debug_write("trigger_channel_scan() enter, device AlMac:"..almac) + local r = c_trigger_channel_scan(almac) + debug_write("trigger_channel_scan() status: "..r.status) + http.write_json(r) +end + +function get_channel_stats() + local r = c_get_channel_stats() + http.write_json(r) +end + +function get_channel_planning_score() + local r = c_get_channel_planning_score() + http.write_json(r) +end + +function apply_channel_utilization_th(channelUtilTh2G, channelUtilTh5GL, channelUtilTh5GH) + assert(channelUtilTh2G) + assert(channelUtilTh5GL) + assert(channelUtilTh5GH) + local r = c_apply_channel_utilization_th(channelUtilTh2G, channelUtilTh5GL, channelUtilTh5GH) + if r.status == "SUCCESS" then + local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + if easymesh_cfgs['CUOverloadTh_2G'] ~= channelUtilTh2G or + easymesh_cfgs['CUOverloadTh_5G_L'] ~= channelUtilTh5GL or + easymesh_cfgs['CUOverloadTh_5G_H'] ~= channelUtilTh5GH then + easymesh_cfgs['CUOverloadTh_2G'] = channelUtilTh2G + easymesh_cfgs['CUOverloadTh_5G_L'] = channelUtilTh5GL + easymesh_cfgs['CUOverloadTh_5G_H'] = channelUtilTh5GH + mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + end + local easymesh_mapd_cfgs = mtkwifi.load_profile(mtkwifi.__easymesh_mapd_profile_path()) + if easymesh_mapd_cfgs['CUOverloadTh_2G'] ~= channelUtilTh2G or + easymesh_mapd_cfgs['CUOverloadTh_5G_L'] ~= channelUtilTh5GL or + easymesh_mapd_cfgs['CUOverloadTh_5G_H'] ~= channelUtilTh5GH then + easymesh_mapd_cfgs['CUOverloadTh_2G'] = channelUtilTh2G + easymesh_mapd_cfgs['CUOverloadTh_5G_L'] = channelUtilTh5GL + easymesh_mapd_cfgs['CUOverloadTh_5G_H'] = channelUtilTh5GH + mtkwifi.save_easymesh_mapd_profile(easymesh_mapd_cfgs) + end + end + http.write_json(r) +end + +function get_sta_bh_interface() + local r = mtkwifi.get_easymesh_on_boarded_iface_info() + http.write_json(r) +end + +function get_ap_bh_inf_list() + local devs = mtkwifi.get_all_devs() + local r = c_get_ap_bh_inf_list() + if r.status == "SUCCESS" then + r['apBhInfListStr'] = "" + for mac in string.gmatch(r.macList, "(%x%x:%x%x:%x%x:%x%x:%x%x:%x%x);") do + for _, dev in ipairs(devs) do + local bssid_without_lf = dev.apcli and dev.apcli.mac_addr:upper():sub(1,17) or "" + if mac:upper() == bssid_without_lf then + r['apBhInfListStr'] = r['apBhInfListStr']..dev.apcli.vifname..';' + else + for _,vif in ipairs(dev.vifs) do + bssid_without_lf = vif.__bssid:upper():sub(1,17) + if mac:upper() == bssid_without_lf then + r['apBhInfListStr'] = r['apBhInfListStr']..vif.vifname..';' + end + end + end + end + end + end + http.write_json(r) +end + +function get_ap_fh_inf_list() + local devs = mtkwifi.get_all_devs() + local r = c_get_ap_fh_inf_list() + if r.status == "SUCCESS" then + r['apFhInfListStr'] = "" + for mac in string.gmatch(r.macList, "(%x%x:%x%x:%x%x:%x%x:%x%x:%x%x);") do + for _, dev in ipairs(devs) do + local bssid_without_lf = dev.apcli and dev.apcli.mac_addr:upper():sub(1,17) or "" + if mac:upper() == bssid_without_lf then + r['apFhInfListStr'] = r['apFhInfListStr']..dev.apcli.vifname..';' + else + for _,vif in ipairs(dev.vifs) do + bssid_without_lf = vif.__bssid:upper():sub(1,17) + if mac:upper() == bssid_without_lf then + r['apFhInfListStr'] = r['apFhInfListStr']..vif.vifname..';' + end + end + end + end + end + end + http.write_json(r) +end + +function validate_easymesh_bss(r, cfgs, alMac, band) + assert(type(r) == 'table') + assert(type(cfgs) == 'table') + assert(type(alMac) == 'string') + assert(type(band) == 'string') + if not cfgs[alMac] then + r['status'] = 'SUCCESS' + elseif not cfgs[alMac][band] then + r['status'] = 'SUCCESS' + else + local numBss = mtkwifi.get_table_length(cfgs[alMac][band]) + if numBss >= 4 then + r['status'] = 'No more BSS could be added!' + else + r['status'] = 'SUCCESS' + end + end +end + +function validate_add_easymesh_bss_req(alMac, band) + local r = {} + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + if type(alMac) ~= 'string' then + r["status"]= "Invalid AL-MAC Type "..type(alMac).." !" + elseif type(band) ~= 'string' then + r["status"]= "Invalid Band Type "..type(band).." !" + else + if type(cfgs) ~= "table" or next(cfgs) == nil then + cfgs = {} + cfgs['wildCardAlMacCfgs'] = {} + cfgs['distinctAlMacCfgs'] = {} + end + if alMac == 'FF:FF:FF:FF:FF:FF' then + validate_easymesh_bss(r, cfgs['wildCardAlMacCfgs'], alMac, band) + else + validate_easymesh_bss(r, cfgs['distinctAlMacCfgs'], alMac, band) + end + end + if type(r) ~= 'table' or next(r) == nil then + r['status'] = "Unexpected Exception in validate_easymesh_bss()!" + end + http.write_json(r) +end + +function apply_easymesh_bss_cfg(isLocal) + local r = c_apply_bss_config_renew() + if r['status'] == 'SUCCESS' then + local easymesh_bss_cfg_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__easymesh_bss_cfgs_path()) + os.execute("cp -f "..mtkwifi.__easymesh_bss_cfgs_path().." "..easymesh_bss_cfg_applied_path) + end + if isLocal then + return r + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap", "easymesh_bss_config_renew")) + end +end + +function get_easymesh_bss_index(bssInfoTbl, bssInfoInp) + assert(type(bssInfoTbl) == 'table') + assert(type(bssInfoInp) == 'table') + for bssIdx, bssInfo in pairs(bssInfoTbl) do + debug_write("get SSID from wts_bss_info_config = "..bssInfo['ssid']) + bssInfoInp['defPCP'] = "N/A" + bssInfoInp['primVlan'] = "N/A" + if bssInfo['ssid'] == bssInfoInp['ssid'] and + bssInfo['authMode'] == bssInfoInp['authMode'] and + bssInfo['encType'] == bssInfoInp['encType'] and + bssInfo['passPhrase'] == bssInfoInp['passPhrase'] and + bssInfo['isBhBssSupported'] == bssInfoInp['isBhBssSupported'] and + bssInfo['isFhBssSupported'] == bssInfoInp['isFhBssSupported'] and + bssInfo['isHidden'] == bssInfoInp['isHidden'] and + bssInfo['fhVlanId'] == bssInfoInp['fhVlanId'] and + bssInfo['primVlan'] == bssInfoInp['primVlan'] and + bssInfo['defPCP'] == bssInfoInp['defPCP'] then + return bssIdx + end + end + return nil +end + +function update_easymesh_bss(cfgs, bssInfoInp, isEdit) + assert(type(cfgs) == 'table') + assert(type(bssInfoInp) == 'table') + assert(type(isEdit) == 'string') + if not cfgs[bssInfoInp['alMac']] then + cfgs[bssInfoInp['alMac']] = {} + end + if not cfgs[bssInfoInp['alMac']][bssInfoInp['band']] then + cfgs[bssInfoInp['alMac']][bssInfoInp['band']] = {} + end + local bssInfoTbl = cfgs[bssInfoInp['alMac']][bssInfoInp['band']] + local bssInfoIdx + if isEdit == "1" then + local editBssInfo = {} + local tmpEditSSID = http.formvalue('__EDIT_SSID'):gsub("\\", "\\\\") + editBssInfo['ssid'] = tmpEditSSID:gsub("%s+","\\ ") + debug_write("get edited SSID from UI = "..editBssInfo['ssid']) + editBssInfo['authMode'] = http.formvalue('__EDIT_AUTH_MODE') + editBssInfo['encType'] = http.formvalue('__EDIT_ENCRYPTION_TYPE') + local tmpEditPassPhrase = http.formvalue('__EDIT_PASS_PHRASE'):gsub("\\", "\\\\") + editBssInfo['passPhrase'] = tmpEditPassPhrase:gsub("%s+","\\ ") + editBssInfo['isBhBssSupported'] = http.formvalue('__EDIT_BH_SUPPORT') + editBssInfo['isFhBssSupported'] = http.formvalue('__EDIT_FH_SUPPORT') + editBssInfo['isHidden'] = http.formvalue('__EDIT_IS_SSID_HIDDEN') + editBssInfo['fhVlanId'] = http.formvalue('__EDIT_FH_VLAN_ID') + editBssInfo['primVlan'] = http.formvalue('__EDIT_PRIM_VLAN') + editBssInfo['defPCP'] = http.formvalue('__EDIT_DEF_PCP') + bssInfoIdx = get_easymesh_bss_index(bssInfoTbl, editBssInfo) + assert(bssInfoIdx) + assert(type(bssInfoTbl[bssInfoIdx]) == 'table') + else + bssInfoIdx = mtkwifi.get_table_length(bssInfoTbl) + 1 + bssInfoTbl[bssInfoIdx] = {} + end + local bssInfo = bssInfoTbl[bssInfoIdx] + bssInfo['ssid'] = bssInfoInp['ssid'] + debug_write("final SSID write to wts_bss_info_config = "..bssInfo['ssid']) + bssInfo['authMode'] = bssInfoInp['authMode'] + bssInfo['encType'] = bssInfoInp['encType'] + bssInfo['passPhrase'] = bssInfoInp['passPhrase'] and bssInfoInp['passPhrase'] ~= '' and bssInfoInp['passPhrase'] or '12345678' + bssInfo['isBhBssSupported'] = bssInfoInp['isBhBssSupported'] + bssInfo['isFhBssSupported'] = bssInfoInp['isFhBssSupported'] + bssInfo['isHidden'] = bssInfoInp['isHidden'] + bssInfo['fhVlanId'] = bssInfoInp['fhVlanId'] + bssInfo['primVlan'] = bssInfoInp['primVlan'] + bssInfo['defPCP'] = bssInfoInp['defPCP'] + +end + +function easymesh_bss_cfg() + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + + local bssInfoInp = {} + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("easymesh_bss_cfg: Input BSSINFO are of incorrect type!",k,v) + elseif string.byte(k) ~= string.byte("_") then + bssInfoInp[k] = v + end + end + + if bssInfoInp['primVlan'] ~= "N/A" and bssInfoInp['defPCP'] ~= "N/A" then + for alMac,alMacTbl in pairs(cfgs['wildCardAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + bssInfo['primVlan'] = "N/A" + bssInfo['defPCP'] = "N/A" + end + end + end + + for alMac,alMacTbl in pairs(cfgs['distinctAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + bssInfo['primVlan'] = "N/A" + bssInfo['defPCP'] = "N/A" + end + end + end + end + + debug_write("original SSID which user entered = "..bssInfoInp['ssid']) + local tmpSSID = bssInfoInp['ssid']:gsub("\\", "\\\\") + bssInfoInp['ssid'] = tmpSSID:gsub("%s+","\\ ") + debug_write("get SSID from UI = "..bssInfoInp['ssid']) + local tmpPassPhrase = bssInfoInp['passPhrase']:gsub("\\", "\\\\") + bssInfoInp['passPhrase'] = tmpPassPhrase:gsub("%s+","\\ ") + if type(cfgs) ~= "table" or next(cfgs) == nil then + cfgs = {} + cfgs['wildCardAlMacCfgs'] = {} + cfgs['distinctAlMacCfgs'] = {} + end + if bssInfoInp['alMac'] == 'FF:FF:FF:FF:FF:FF' then + update_easymesh_bss(cfgs['wildCardAlMacCfgs'], bssInfoInp, http.formvalue('__IS_EDIT')) + else + update_easymesh_bss(cfgs['distinctAlMacCfgs'], bssInfoInp, http.formvalue('__IS_EDIT')) + end + mtkwifi.save_easymesh_bss_cfgs(cfgs) + if http.formvalue("__apply") then + apply_easymesh_bss_cfg(true) + end + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap", "easymesh_bss_config_renew")) +end + +function remove_easymesh_bss(r,cfgs,bssInfoInp) + assert(type(r) == 'table') + assert(type(cfgs) == 'table') + assert(type(bssInfoInp) == 'table') + for alMac,alMacTbl in pairs(cfgs) do + if alMac == bssInfoInp['alMac'] then + assert(type(alMacTbl) == 'table') + for band,bssInfoTbl in pairs(alMacTbl) do + if bssInfoInp['primVlan'] ~= "N/A" and bssInfoInp['defPCP'] ~= "N/A" then + for _,bssInfo in pairs(bssInfoTbl) do + bssInfo['primVlan'] = "N/A" + bssInfo['defPCP'] = "N/A" + end + end + if band == bssInfoInp['band'] then + assert(type(bssInfoTbl) == 'table') + local bssIdx = get_easymesh_bss_index(bssInfoTbl, bssInfoInp) + if bssIdx then + local alMacTblLen = mtkwifi.get_table_length(alMacTbl) + local bssInfoTblLen = mtkwifi.get_table_length(bssInfoTbl) + if bssInfoTblLen == 1 then + cfgs[alMac][band] = nil + if alMacTblLen == 1 then + cfgs[alMac] = nil + end + else + table.remove(cfgs[alMac][band], tonumber(bssIdx)) + end + r['status'] = 'SUCCESS' + else + r['status'] = 'ERROR: BSSINFO does not exist!' + end + break + end + end + if next(r) == nil then + r['status'] = 'ERROR: BAND does not exist!' + end + break + end + end + if next(r) == nil then + r['status'] = 'ERROR: AL-MAC does not exist!' + end +end + +function remove_easymesh_bss_cfg_req() + local r = {} + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + if type(cfgs) ~= "table" or next(cfgs) == nil then + r["status"]= mtkwifi.__easymesh_bss_cfgs_path().." file not found!" + else + local bssInfoInp = {} + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + r["status"]= "Input BSSINFO are of incorrect type!" + break + elseif string.byte(k) ~= string.byte("_") then + bssInfoInp[k] = v + end + end + local tmpSSID = bssInfoInp['ssid']:gsub("\\", "\\\\") + bssInfoInp['ssid'] = tmpSSID:gsub("%s+","\\ ") + local tmpPassPhrase = bssInfoInp['passPhrase']:gsub("\\", "\\\\") + bssInfoInp['passPhrase'] = tmpPassPhrase:gsub("%s+","\\ ") + if next(r) == nil then + if bssInfoInp['alMac'] == 'FF:FF:FF:FF:FF:FF' then + remove_easymesh_bss(r, cfgs['wildCardAlMacCfgs'], bssInfoInp) + else + remove_easymesh_bss(r, cfgs['distinctAlMacCfgs'], bssInfoInp) + end + end + end + if type(r) ~= 'table' or next(r) == nil then + r['status'] = "Unexpected Exception in remove_easymesh_bss()!" + else + mtkwifi.save_easymesh_bss_cfgs(cfgs) + r = apply_easymesh_bss_cfg(true) + end + http.write_json(r) +end + +function get_user_preferred_channel() + local r = c_get_user_preferred_channel() + http.write_json(r) +end + +function get_sp_rule_list() + local r = c_get_sp_rule_list() + http.write_json(r) +end + +function del_sp_rule(index) + if index == nil then + index = "" + end + local r = c_del_sp_rule(index) + http.write_json(r) +end + +function sp_rule_reorder(index1, index2) + local r = c_sp_rule_reorder(index1, index2) + http.write_json(r) +end + +function sp_rule_move(index, action) + local r = c_sp_rule_move(index, action) + http.write_json(r) +end + +function sp_rule_add(str_rule) + str_rule = string.gsub(str_rule, "] ", "]+") + local r = c_sp_rule_add(str_rule) + http.write_json(r) +end + +function sp_config_done() + local r = c_sp_config_done() + http.write_json(r) +end + +function submit_dpp_uri() + uri = http.formvalue("uri") + os.execute("wappctrl ra0 dpp dpp_qr_code ".."\""..uri.."\"") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) +end + +function start_dpp_onboarding() + os.execute("wappctrl ra0 dpp dpp_start") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) +end + +function generate_dpp_uri() + os.execute("wappctrl ra0 dpp dpp_bootstrap_gen type=qrcode") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) +end + +function retrive_dpp_uri() + local result = mtkwifi.read_pipe(tostring("mapd_cli /tmp/mapd_ctrl get_dpp_uri").." 2>&1") + result = result:gsub("<", "<") + http.write(tostring(result)) +end \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.luac new file mode 100644 index 000000000000..7ab07b9ccea9 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.lua new file mode 100644 index 000000000000..8ff1bb69818b --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.lua @@ -0,0 +1,37 @@ +local debug = require "debug" +local io = require "io" +local collectgarbage, floor = collectgarbage, math.floor + +module "luci.debug" +__file__ = debug.getinfo(1, 'S').source:sub(2) + +-- Enables the memory tracer with given flags and returns a function to disable the tracer again +function trap_memtrace(flags, dest) + flags = flags or "clr" + local tracefile = io.open(dest or "/tmp/memtrace", "w") + local peak = 0 + + local function trap(what, line) + local info = debug.getinfo(2, "Sn") + local size = floor(collectgarbage("count")) + if size > peak then + peak = size + end + if tracefile then + tracefile:write( + "[", what, "] ", info.source, ":", (line or "?"), "\t", + (info.namewhat or ""), "\t", + (info.name or ""), "\t", + size, " (", peak, ")\n" + ) + end + end + + debug.sethook(trap, flags) + + return function() + debug.sethook() + tracefile:close() + end +end + diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.luac new file mode 100644 index 000000000000..b4421125bcbc Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.lua new file mode 100644 index 000000000000..bd1b112f60cd --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.lua @@ -0,0 +1,1532 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008-2015 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local fs = require "nixio.fs" +local sys = require "luci.sys" +local util = require "luci.util" +local xml = require "luci.xml" +local http = require "luci.http" +local nixio = require "nixio", require "nixio.util" + +module("luci.dispatcher", package.seeall) +context = util.threadlocal() +uci = require "luci.model.uci" +i18n = require "luci.i18n" +_M.fs = fs + +-- Index table +local index = nil + +local function check_fs_depends(spec) + local fs = require "nixio.fs" + + for path, kind in pairs(spec) do + if kind == "directory" then + local empty = true + for entry in (fs.dir(path) or function() end) do + empty = false + break + end + if empty then + return false + end + elseif kind == "executable" then + if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then + return false + end + elseif kind == "file" then + if fs.stat(path, "type") ~= "reg" then + return false + end + end + end + + return true +end + +local function check_uci_depends_options(conf, s, opts) + local uci = require "luci.model.uci" + + if type(opts) == "string" then + return (s[".type"] == opts) + elseif opts == true then + for option, value in pairs(s) do + if option:byte(1) ~= 46 then + return true + end + end + elseif type(opts) == "table" then + for option, value in pairs(opts) do + local sval = s[option] + if type(sval) == "table" then + local found = false + for _, v in ipairs(sval) do + if v == value then + found = true + break + end + end + if not found then + return false + end + elseif value == true then + if sval == nil then + return false + end + else + if sval ~= value then + return false + end + end + end + end + + return true +end + +local function check_uci_depends_section(conf, sect) + local uci = require "luci.model.uci" + + for section, options in pairs(sect) do + local stype = section:match("^@([A-Za-z0-9_%-]+)$") + if stype then + local found = false + uci:foreach(conf, stype, function(s) + if check_uci_depends_options(conf, s, options) then + found = true + return false + end + end) + if not found then + return false + end + else + local s = uci:get_all(conf, section) + if not s or not check_uci_depends_options(conf, s, options) then + return false + end + end + end + + return true +end + +local function check_uci_depends(conf) + local uci = require "luci.model.uci" + + for config, values in pairs(conf) do + if values == true then + local found = false + uci:foreach(config, nil, function(s) + found = true + return false + end) + if not found then + return false + end + elseif type(values) == "table" then + if not check_uci_depends_section(config, values) then + return false + end + end + end + + return true +end + +local function check_acl_depends(require_groups, groups) + if type(require_groups) == "table" and #require_groups > 0 then + local writable = false + + for _, group in ipairs(require_groups) do + local read = false + local write = false + if type(groups) == "table" and type(groups[group]) == "table" then + for _, perm in ipairs(groups[group]) do + if perm == "read" then + read = true + elseif perm == "write" then + write = true + end + end + end + if not read and not write then + return nil + elseif write then + writable = true + end + end + + return writable + end + + return true +end + +local function check_depends(spec) + if type(spec.depends) ~= "table" then + return true + end + + if type(spec.depends.fs) == "table" then + local satisfied = false + local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs } + for _, alternative in ipairs(alternatives) do + if check_fs_depends(alternative) then + satisfied = true + break + end + end + if not satisfied then + return false + end + end + + if type(spec.depends.uci) == "table" then + local satisfied = false + local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci } + for _, alternative in ipairs(alternatives) do + if check_uci_depends(alternative) then + satisfied = true + break + end + end + if not satisfied then + return false + end + end + + return true +end + +local function target_to_json(target, module) + local action + + if target.type == "call" then + action = { + ["type"] = "call", + ["module"] = module, + ["function"] = target.name, + ["parameters"] = target.argv + } + elseif target.type == "view" then + action = { + ["type"] = "view", + ["path"] = target.view + } + elseif target.type == "template" then + action = { + ["type"] = "template", + ["path"] = target.view + } + elseif target.type == "cbi" then + action = { + ["type"] = "cbi", + ["path"] = target.model, + ["config"] = target.config + } + elseif target.type == "form" then + action = { + ["type"] = "form", + ["path"] = target.model + } + elseif target.type == "firstchild" then + action = { + ["type"] = "firstchild" + } + elseif target.type == "firstnode" then + action = { + ["type"] = "firstchild", + ["recurse"] = true + } + elseif target.type == "arcombine" then + if type(target.targets) == "table" then + action = { + ["type"] = "arcombine", + ["targets"] = { + target_to_json(target.targets[1], module), + target_to_json(target.targets[2], module) + } + } + end + elseif target.type == "alias" then + action = { + ["type"] = "alias", + ["path"] = table.concat(target.req, "/") + } + elseif target.type == "rewrite" then + action = { + ["type"] = "rewrite", + ["path"] = table.concat(target.req, "/"), + ["remove"] = target.n + } + end + + if target.post and action then + action.post = target.post + end + + return action +end + +local function tree_to_json(node, json) + local fs = require "nixio.fs" + local util = require "luci.util" + + if type(node.nodes) == "table" then + for subname, subnode in pairs(node.nodes) do + local spec = { + title = xml.striptags(subnode.title), + order = subnode.order + } + + if subnode.leaf then + spec.wildcard = true + end + + if subnode.cors then + spec.cors = true + end + + if subnode.setuser then + spec.setuser = subnode.setuser + end + + if subnode.setgroup then + spec.setgroup = subnode.setgroup + end + + if type(subnode.target) == "table" then + spec.action = target_to_json(subnode.target, subnode.module) + end + + if type(subnode.file_depends) == "table" then + for _, v in ipairs(subnode.file_depends) do + spec.depends = spec.depends or {} + spec.depends.fs = spec.depends.fs or {} + + local ft = fs.stat(v, "type") + if ft == "dir" then + spec.depends.fs[v] = "directory" + elseif v:match("/s?bin/") then + spec.depends.fs[v] = "executable" + else + spec.depends.fs[v] = "file" + end + end + end + + if type(subnode.uci_depends) == "table" then + for k, v in pairs(subnode.uci_depends) do + spec.depends = spec.depends or {} + spec.depends.uci = spec.depends.uci or {} + spec.depends.uci[k] = v + end + end + + if type(subnode.acl_depends) == "table" then + for _, acl in ipairs(subnode.acl_depends) do + spec.depends = spec.depends or {} + spec.depends.acl = spec.depends.acl or {} + spec.depends.acl[#spec.depends.acl + 1] = acl + end + end + + if (subnode.sysauth_authenticator ~= nil) or + (subnode.sysauth ~= nil and subnode.sysauth ~= false) + then + if subnode.sysauth_authenticator == "htmlauth" then + spec.auth = { + login = true, + methods = { "cookie:sysauth" } + } + elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then + spec.auth = { + login = false, + methods = { "query:auth", "cookie:sysauth" } + } + elseif subnode.module == "luci.controller.admin.uci" then + spec.auth = { + login = false, + methods = { "param:sid" } + } + end + elseif subnode.sysauth == false then + spec.auth = {} + end + + if not spec.action then + spec.title = nil + end + + spec.satisfied = check_depends(spec) + json.children = json.children or {} + json.children[subname] = tree_to_json(subnode, spec) + end + end + + return json +end + +function build_url(...) + local path = {...} + local url = { http.getenv("SCRIPT_NAME") or "" } + + local p + for _, p in ipairs(path) do + if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then + url[#url+1] = "/" + url[#url+1] = p + end + end + + if #path == 0 then + url[#url+1] = "/" + end + + return table.concat(url, "") +end + + +function error404(message) + http.status(404, "Not Found") + message = message or "Not Found" + + local function render() + local template = require "luci.template" + template.render("error404", {message=message}) + end + + if not util.copcall(render) then + http.prepare_content("text/plain") + http.write(message) + end + + return false +end + +function error500(message) + util.perror(message) + if not context.template_header_sent then + http.status(500, "Internal Server Error") + http.prepare_content("text/plain") + http.write(message) + else + require("luci.template") + if not util.copcall(luci.template.render, "error500", {message=message}) then + http.prepare_content("text/plain") + http.write(message) + end + end + return false +end + +local function determine_request_language() + local conf = require "luci.config" + assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'") + + local lang = conf.main.lang or "auto" + if lang == "auto" then + local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" + for aclang in aclang:gmatch("[%w_-]+") do + local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$") + if country and culture then + local cc = "%s_%s" %{ country, culture:lower() } + if conf.languages[cc] then + lang = cc + break + elseif conf.languages[country] then + lang = country + break + end + elseif conf.languages[aclang] then + lang = aclang + break + end + end + end + + if lang == "auto" then + lang = i18n.default + end + + i18n.setlanguage(lang) +end + +function httpdispatch(request, prefix) + http.context.request = request + + local r = {} + context.request = r + + local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true) + + if prefix then + for _, node in ipairs(prefix) do + r[#r+1] = node + end + end + + local node + for node in pathinfo:gmatch("[^/%z]+") do + r[#r+1] = node + end + + determine_request_language() + + local stat, err = util.coxpcall(function() + dispatch(context.request) + end, error500) + + http.close() + + --context._disable_memtrace() +end + +local function require_post_security(target, args) + if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then + return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args) + end + + if type(target) == "table" then + if type(target.post) == "table" then + local param_name, required_val, request_val + + for param_name, required_val in pairs(target.post) do + request_val = http.formvalue(param_name) + + if (type(required_val) == "string" and + request_val ~= required_val) or + (required_val == true and request_val == nil) + then + return false + end + end + + return true + end + + return (target.post == true) + end + + return false +end + +function test_post_security() + if http.getenv("REQUEST_METHOD") ~= "POST" then + http.status(405, "Method Not Allowed") + http.header("Allow", "POST") + return false + end + + if http.formvalue("token") ~= context.authtoken then + http.status(403, "Forbidden") + luci.template.render("csrftoken") + return false + end + + return true +end + +local function session_retrieve(sid, allowed_users) + local sdat = util.ubus("session", "get", { ubus_rpc_session = sid }) + local sacl = util.ubus("session", "access", { ubus_rpc_session = sid }) + + if type(sdat) == "table" and + type(sdat.values) == "table" and + type(sdat.values.token) == "string" and + (not allowed_users or + util.contains(allowed_users, sdat.values.username)) + then + uci:set_session_id(sid) + return sid, sdat.values, type(sacl) == "table" and sacl or {} + end + + return nil, nil, nil +end + +local function session_setup(user, pass) + local login = util.ubus("session", "login", { + username = user, + password = pass, + timeout = tonumber(luci.config.sauth.sessiontime) + }) + + local rp = context.requestpath + and table.concat(context.requestpath, "/") or "" + + if type(login) == "table" and + type(login.ubus_rpc_session) == "string" + then + util.ubus("session", "set", { + ubus_rpc_session = login.ubus_rpc_session, + values = { token = sys.uniqueid(16) } + }) + nixio.syslog("info", tostring("luci: accepted login on /%s for %s from %s\n" + %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })) + + return session_retrieve(login.ubus_rpc_session) + end + nixio.syslog("info", tostring("luci: failed login on /%s for %s from %s\n" + %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })) +end + +local function check_authentication(method) + local auth_type, auth_param = method:match("^(%w+):(.+)$") + local sid, sdat + + if auth_type == "cookie" then + sid = http.getcookie(auth_param) + elseif auth_type == "param" then + sid = http.formvalue(auth_param) + elseif auth_type == "query" then + sid = http.formvalue(auth_param, true) + end + + return session_retrieve(sid) +end + +local function get_children(node) + local children = {} + + if not node.wildcard and type(node.children) == "table" then + for name, child in pairs(node.children) do + children[#children+1] = { + name = name, + node = child, + order = child.order or 1000 + } + end + + table.sort(children, function(a, b) + if a.order == b.order then + return a.name < b.name + else + return a.order < b.order + end + end) + end + + return children +end + +local function find_subnode(root, prefix, recurse, descended) + local children = get_children(root) + + if #children > 0 and (not descended or recurse) then + local sub_path = { unpack(prefix) } + + if recurse == false then + recurse = nil + end + + for _, child in ipairs(children) do + sub_path[#prefix+1] = child.name + + local res_path = find_subnode(child.node, sub_path, recurse, true) + + if res_path then + return res_path + end + end + end + + if descended then + if not recurse or + root.action.type == "cbi" or + root.action.type == "form" or + root.action.type == "view" or + root.action.type == "template" or + root.action.type == "arcombine" + then + return prefix + end + end +end + +local function merge_trees(node_a, node_b) + for k, v in pairs(node_b) do + if k == "children" then + node_a.children = node_a.children or {} + + for name, spec in pairs(v) do + node_a.children[name] = merge_trees(node_a.children[name] or {}, spec) + end + else + node_a[k] = v + end + end + + if type(node_a.action) == "table" and + node_a.action.type == "firstchild" and + node_a.children == nil + then + node_a.satisfied = false + end + + return node_a +end + +local function apply_tree_acls(node, acl) + if type(node.children) == "table" then + for _, child in pairs(node.children) do + apply_tree_acls(child, acl) + end + end + + local perm + if type(node.depends) == "table" then + perm = check_acl_depends(node.depends.acl, acl["access-group"]) + else + perm = true + end + + if perm == nil then + node.satisfied = false + elseif perm == false then + node.readonly = true + end +end + +function menu_json(acl) + local tree = context.tree or createtree() + local lua_tree = tree_to_json(tree, { + action = { + ["type"] = "firstchild", + ["recurse"] = true + } + }) + + local json_tree = createtree_json() + local menu_tree = merge_trees(lua_tree, json_tree) + + if acl then + apply_tree_acls(menu_tree, acl) + end + + return menu_tree +end + +local function init_template_engine(ctx) + local tpl = require "luci.template" + local media = luci.config.main.mediaurlbase + + if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then + media = nil + for name, theme in pairs(luci.config.themes) do + if name:sub(1,1) ~= "." and pcall(tpl.Template, + "themes/%s/header" % fs.basename(theme)) then + media = theme + end + end + assert(media, "No valid theme found") + end + + local function _ifattr(cond, key, val, noescape) + if cond then + local env = getfenv(3) + local scope = (type(env.self) == "table") and env.self + if type(val) == "table" then + if not next(val) then + return '' + else + val = util.serialize_json(val) + end + end + + val = tostring(val or + (type(env[key]) ~= "function" and env[key]) or + (scope and type(scope[key]) ~= "function" and scope[key]) or "") + + if noescape ~= true then + val = xml.pcdata(val) + end + + return string.format(' %s="%s"', tostring(key), val) + else + return '' + end + end + + tpl.context.viewns = setmetatable({ + write = http.write; + include = function(name) tpl.Template(name):render(getfenv(2)) end; + translate = i18n.translate; + translatef = i18n.translatef; + export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end; + striptags = xml.striptags; + pcdata = xml.pcdata; + media = media; + theme = fs.basename(media); + resource = luci.config.main.resourcebase; + ifattr = function(...) return _ifattr(...) end; + attr = function(...) return _ifattr(true, ...) end; + url = build_url; + }, {__index=function(tbl, key) + if key == "controller" then + return build_url() + elseif key == "REQUEST_URI" then + return build_url(unpack(ctx.requestpath)) + elseif key == "FULL_REQUEST_URI" then + local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") } + local query = http.getenv("QUERY_STRING") + if query and #query > 0 then + url[#url+1] = "?" + url[#url+1] = query + end + return table.concat(url, "") + elseif key == "token" then + return ctx.authtoken + else + return rawget(tbl, key) or _G[key] + end + end}) + + return tpl +end + +function dispatch(request) + --context._disable_memtrace = require "luci.debug".trap_memtrace("l") + local ctx = context + + local auth, cors, suid, sgid + local menu = menu_json() + local page = menu + + local requested_path_full = {} + local requested_path_node = {} + local requested_path_args = {} + + local required_path_acls = {} + + for i, s in ipairs(request) do + if type(page.children) ~= "table" or not page.children[s] then + page = nil + break + end + + if not page.children[s].satisfied then + page = nil + break + end + + page = page.children[s] + auth = page.auth or auth + cors = page.cors or cors + suid = page.setuser or suid + sgid = page.setgroup or sgid + + if type(page.depends) == "table" and type(page.depends.acl) == "table" then + for _, group in ipairs(page.depends.acl) do + local found = false + for _, item in ipairs(required_path_acls) do + if item == group then + found = true + break + end + end + if not found then + required_path_acls[#required_path_acls + 1] = group + end + end + end + + requested_path_full[i] = s + requested_path_node[i] = s + + if page.wildcard then + for j = i + 1, #request do + requested_path_args[j - i] = request[j] + requested_path_full[j] = request[j] + end + break + end + end + + local tpl = init_template_engine(ctx) + + ctx.args = requested_path_args + ctx.path = requested_path_node + ctx.dispatched = page + + ctx.requestpath = ctx.requestpath or requested_path_full + ctx.requestargs = ctx.requestargs or requested_path_args + ctx.requested = ctx.requested or page + + if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then + local sid, sdat, sacl + for _, method in ipairs(auth.methods) do + sid, sdat, sacl = check_authentication(method) + + if sid and sdat and sacl then + break + end + end + + if not (sid and sdat and sacl) and auth.login then + local user = http.getenv("HTTP_AUTH_USER") + local pass = http.getenv("HTTP_AUTH_PASS") + + if user == nil and pass == nil then + user = http.formvalue("luci_username") + pass = http.formvalue("luci_password") + end + + if user and pass then + sid, sdat, sacl = session_setup(user, pass) + end + + if not sid then + context.path = {} + + http.status(403, "Forbidden") + http.header("X-LuCI-Login-Required", "yes") + + local scope = { duser = "root", fuser = user } + local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope) + if ok then + return res + end + return tpl.render("sysauth", scope) + end + + http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{ + sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or "" + }) + + http.redirect(build_url(unpack(ctx.requestpath))) + return + end + + if not sid or not sdat or not sacl then + http.status(403, "Forbidden") + http.header("X-LuCI-Login-Required", "yes") + return + end + + ctx.authsession = sid + ctx.authtoken = sdat.token + ctx.authuser = sdat.username + ctx.authacl = sacl + end + + if #required_path_acls > 0 then + local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"]) + if perm == nil then + http.status(403, "Forbidden") + return + end + + if page then + page.readonly = not perm + end + end + + local action = (page and type(page.action) == "table") and page.action or {} + + if action.type == "arcombine" then + action = (#requested_path_args > 0) and action.targets[2] or action.targets[1] + end + + if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then + luci.http.status(200, "OK") + luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*") + luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + return + end + + if require_post_security(action) then + if not test_post_security() then + return + end + end + + if sgid then + sys.process.setgroup(sgid) + end + + if suid then + sys.process.setuser(suid) + end + + if action.type == "view" then + tpl.render("view", { view = action.path }) + + elseif action.type == "call" then + local ok, mod = util.copcall(require, action.module) + if not ok then + error500(mod) + return + end + + local func = mod[action["function"]] + + assert(func ~= nil, + 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?') + + assert(type(func) == "function", + 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' .. + 'of type "' .. type(func) .. '".') + + local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {} + for _, s in ipairs(requested_path_args) do + argv[#argv + 1] = s + end + + local ok, err = util.copcall(func, unpack(argv)) + if not ok then + error500(err) + end + + elseif action.type == "firstchild" then + local sub_request = find_subnode(page, requested_path_full, action.recurse) + if sub_request then + dispatch(sub_request) + else + tpl.render("empty_node_placeholder", getfenv(1)) + end + + elseif action.type == "alias" then + local sub_request = {} + for name in action.path:gmatch("[^/]+") do + sub_request[#sub_request + 1] = name + end + + for _, s in ipairs(requested_path_args) do + sub_request[#sub_request + 1] = s + end + + dispatch(sub_request) + + elseif action.type == "rewrite" then + local sub_request = { unpack(request) } + for i = 1, action.remove do + table.remove(sub_request, 1) + end + + local n = 1 + for s in action.path:gmatch("[^/]+") do + table.insert(sub_request, n, s) + n = n + 1 + end + + for _, s in ipairs(requested_path_args) do + sub_request[#sub_request + 1] = s + end + + dispatch(sub_request) + + elseif action.type == "template" then + tpl.render(action.path, getfenv(1)) + + elseif action.type == "cbi" then + _cbi({ config = action.config, model = action.path }, unpack(requested_path_args)) + + elseif action.type == "form" then + _form({ model = action.path }, unpack(requested_path_args)) + + else + local root = find_subnode(menu, {}, true) + if not root then + error404("No root node was registered, this usually happens if no module was installed.\n" .. + "Install luci-mod-admin-full and retry. " .. + "If the module is already installed, try removing the /tmp/luci-indexcache file.") + else + error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" .. + "If this url belongs to an extension, make sure it is properly installed.\n" .. + "If the extension was recently installed, try removing the /tmp/luci-indexcache file.") + end + end +end + +local function hash_filelist(files) + local fprint = {} + local n = 0 + + for i, file in ipairs(files) do + local st = fs.stat(file) + if st then + fprint[n + 1] = '%x' % st.ino + fprint[n + 2] = '%x' % st.mtime + fprint[n + 3] = '%x' % st.size + n = n + 3 + end + end + + return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".") +end + +local function read_cachefile(file, reader) + local euid = sys.process.info("uid") + local fuid = fs.stat(file, "uid") + local mode = fs.stat(file, "modestr") + + if euid ~= fuid or mode ~= "rw-------" then + return nil + end + + return reader(file) +end + +function createindex() + local controllers = { } + local base = "%s/controller/" % util.libpath() + local _, path + + for path in (fs.glob("%s*.lua" % base) or function() end) do + controllers[#controllers+1] = path + end + + for path in (fs.glob("%s*/*.lua" % base) or function() end) do + controllers[#controllers+1] = path + end + + local cachefile + + if indexcache then + cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) } + + local res = read_cachefile(cachefile, function(path) return loadfile(path)() end) + if res then + index = res + return res + end + + for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do + fs.unlink(file) + end + end + + index = {} + + for _, path in ipairs(controllers) do + local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".") + local mod = require(modname) + assert(mod ~= true, + "Invalid controller file found\n" .. + "The file '" .. path .. "' contains an invalid module line.\n" .. + "Please verify whether the module name is set to '" .. modname .. + "' - It must correspond to the file path!") + + local idx = mod.index + if type(idx) == "function" then + index[modname] = idx + end + end + + if cachefile then + local f = nixio.open(cachefile, "w", 600) + f:writeall(util.get_bytecode(index)) + f:close() + end +end + +function createtree_json() + local json = require "luci.jsonc" + local tree = {} + + local schema = { + action = "table", + auth = "table", + cors = "boolean", + depends = "table", + order = "number", + setgroup = "string", + setuser = "string", + title = "string", + wildcard = "boolean" + } + + local files = {} + local cachefile + + for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do + files[#files+1] = file + end + + if indexcache then + cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) } + + local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end) + if res then + return res + end + + for file in (fs.glob("%s.*.json" % indexcache) or function() end) do + fs.unlink(file) + end + end + + for _, file in ipairs(files) do + local data = json.parse(fs.readfile(file) or "") + if type(data) == "table" then + for path, spec in pairs(data) do + if type(spec) == "table" then + local node = tree + + for s in path:gmatch("[^/]+") do + if s == "*" then + node.wildcard = true + break + end + + node.children = node.children or {} + node.children[s] = node.children[s] or {} + node = node.children[s] + end + + if node ~= tree then + for k, t in pairs(schema) do + if type(spec[k]) == t then + node[k] = spec[k] + end + end + + node.satisfied = check_depends(spec) + end + end + end + end + end + + if cachefile then + local f = nixio.open(cachefile, "w", 600) + f:writeall(json.stringify(tree)) + f:close() + end + + return tree +end + +-- Build the index before if it does not exist yet. +function createtree() + if not index then + createindex() + end + + local ctx = context + local tree = {nodes={}, inreq=true} + + ctx.treecache = setmetatable({}, {__mode="v"}) + ctx.tree = tree + + local scope = setmetatable({}, {__index = luci.dispatcher}) + + for k, v in pairs(index) do + scope._NAME = k + setfenv(v, scope) + v() + end + + return tree +end + +function assign(path, clone, title, order) + local obj = node(unpack(path)) + obj.nodes = nil + obj.module = nil + + obj.title = title + obj.order = order + + setmetatable(obj, {__index = _create_node(clone)}) + + return obj +end + +function entry(path, target, title, order) + local c = node(unpack(path)) + + c.target = target + c.title = title + c.order = order + c.module = getfenv(2)._NAME + + return c +end + +-- enabling the node. +function get(...) + return _create_node({...}) +end + +function node(...) + local c = _create_node({...}) + + c.module = getfenv(2)._NAME + c.auto = nil + + return c +end + +function lookup(...) + local i, path = nil, {} + for i = 1, select('#', ...) do + local name, arg = nil, tostring(select(i, ...)) + for name in arg:gmatch("[^/]+") do + path[#path+1] = name + end + end + + for i = #path, 1, -1 do + local node = context.treecache[table.concat(path, ".", 1, i)] + if node and (i == #path or node.leaf) then + return node, build_url(unpack(path)) + end + end +end + +function _create_node(path) + if #path == 0 then + return context.tree + end + + local name = table.concat(path, ".") + local c = context.treecache[name] + + if not c then + local last = table.remove(path) + local parent = _create_node(path) + + c = {nodes={}, auto=true, inreq=true} + + parent.nodes[last] = c + context.treecache[name] = c + end + + return c +end + +-- Subdispatchers -- + +function firstchild() + return { type = "firstchild" } +end + +function firstnode() + return { type = "firstnode" } +end + +function alias(...) + return { type = "alias", req = { ... } } +end + +function rewrite(n, ...) + return { type = "rewrite", n = n, req = { ... } } +end + +function call(name, ...) + return { type = "call", argv = {...}, name = name } +end + +function post_on(params, name, ...) + return { + type = "call", + post = params, + argv = { ... }, + name = name + } +end + +function post(...) + return post_on(true, ...) +end + + +function template(name) + return { type = "template", view = name } +end + +function view(name) + return { type = "view", view = name } +end + + +function _cbi(self, ...) + local cbi = require "luci.cbi" + local tpl = require "luci.template" + local http = require "luci.http" + local util = require "luci.util" + + local config = self.config or {} + local maps = cbi.load(self.model, ...) + + local state = nil + + local function has_uci_access(config, level) + local rv = util.ubus("session", "access", { + ubus_rpc_session = context.authsession, + scope = "uci", object = config, + ["function"] = level + }) + + return (type(rv) == "table" and rv.access == true) or false + end + + local i, res + for i, res in ipairs(maps) do + if util.instanceof(res, cbi.SimpleForm) then + io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n" + % self.model) + + io.stderr:write("please change %s to use the form() action instead.\n" + % table.concat(context.request, "/")) + end + + res.flow = config + local cstate = res:parse() + if cstate and (not state or cstate < state) then + state = cstate + end + end + + local function _resolve_path(path) + return type(path) == "table" and build_url(unpack(path)) or path + end + + if config.on_valid_to and state and state > 0 and state < 2 then + http.redirect(_resolve_path(config.on_valid_to)) + return + end + + if config.on_changed_to and state and state > 1 then + http.redirect(_resolve_path(config.on_changed_to)) + return + end + + if config.on_success_to and state and state > 0 then + http.redirect(_resolve_path(config.on_success_to)) + return + end + + if config.state_handler then + if not config.state_handler(state, maps) then + return + end + end + + http.header("X-CBI-State", state or 0) + + if not config.noheader then + tpl.render("cbi/header", {state = state}) + end + + local redirect + local messages + local applymap = false + local pageaction = true + local parsechain = { } + local writable = false + + for i, res in ipairs(maps) do + if res.apply_needed and res.parsechain then + local c + for _, c in ipairs(res.parsechain) do + parsechain[#parsechain+1] = c + end + applymap = true + end + + if res.redirect then + redirect = redirect or res.redirect + end + + if res.pageaction == false then + pageaction = false + end + + if res.message then + messages = messages or { } + messages[#messages+1] = res.message + end + end + + for i, res in ipairs(maps) do + local is_readable_map = has_uci_access(res.config, "read") + local is_writable_map = has_uci_access(res.config, "write") + + writable = writable or is_writable_map + + res:render({ + firstmap = (i == 1), + redirect = redirect, + messages = messages, + pageaction = pageaction, + parsechain = parsechain, + readable = is_readable_map, + writable = is_writable_map + }) + end + + if not config.nofooter then + tpl.render("cbi/footer", { + flow = config, + pageaction = pageaction, + redirect = redirect, + state = state, + autoapply = config.autoapply, + trigger_apply = applymap, + writable = writable + }) + end +end + +function cbi(model, config) + return { + type = "cbi", + post = { ["cbi.submit"] = true }, + config = config, + model = model + } +end + + +function arcombine(trg1, trg2) + return { + type = "arcombine", + env = getfenv(), + targets = {trg1, trg2} + } +end + + +function _form(self, ...) + local cbi = require "luci.cbi" + local tpl = require "luci.template" + local http = require "luci.http" + + local maps = luci.cbi.load(self.model, ...) + local state = nil + + local i, res + for i, res in ipairs(maps) do + local cstate = res:parse() + if cstate and (not state or cstate < state) then + state = cstate + end + end + + http.header("X-CBI-State", state or 0) + tpl.render("header") + for i, res in ipairs(maps) do + res:render() + end + tpl.render("footer") +end + +function form(model) + return { + type = "form", + post = { ["cbi.submit"] = true }, + model = model + } +end + +translate = i18n.translate + +-- This function does not actually translate the given argument but +-- is used by build/i18n-scan.pl to find translatable entries. +function _(text) + return text +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.luac new file mode 100644 index 000000000000..39cb4ec63a98 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.lua new file mode 100644 index 000000000000..20b55f2854ff --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.lua @@ -0,0 +1,554 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2010-2018 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +local coroutine = require "coroutine" +local table = require "table" +local lhttp = require "lucihttp" +local nixio = require "nixio" +local ltn12 = require "luci.ltn12" + +local table, ipairs, pairs, type, tostring, tonumber, error = + table, ipairs, pairs, type, tostring, tonumber, error + +module "luci.http" + +HTTP_MAX_CONTENT = 1024*100 -- 100 kB maximum content size + +context = util.threadlocal() + +Request = util.class() +function Request.__init__(self, env, sourcein, sinkerr) + self.input = sourcein + self.error = sinkerr + + + -- File handler nil by default to let .content() work + self.filehandler = nil + + -- HTTP-Message table + self.message = { + env = env, + headers = {}, + params = urldecode_params(env.QUERY_STRING or ""), + } + + self.parsed_input = false +end + +function Request.formvalue(self, name, noparse) + if not noparse and not self.parsed_input then + self:_parse_input() + end + + if name then + return self.message.params[name] + else + return self.message.params + end +end + +function Request.formvaluetable(self, prefix) + local vals = {} + prefix = prefix and prefix .. "." or "." + + if not self.parsed_input then + self:_parse_input() + end + + local void = self.message.params[nil] + for k, v in pairs(self.message.params) do + if k:find(prefix, 1, true) == 1 then + vals[k:sub(#prefix + 1)] = tostring(v) + end + end + + return vals +end + +function Request.content(self) + if not self.parsed_input then + self:_parse_input() + end + + return self.message.content, self.message.content_length +end + +function Request.getcookie(self, name) + return lhttp.header_attribute("cookie; " .. (self:getenv("HTTP_COOKIE") or ""), name) +end + +function Request.getenv(self, name) + if name then + return self.message.env[name] + else + return self.message.env + end +end + +function Request.setfilehandler(self, callback) + self.filehandler = callback + + if not self.parsed_input then + return + end + + -- If input has already been parsed then uploads are stored as unlinked + -- temporary files pointed to by open file handles in the parameter + -- value table. Loop all params, and invoke the file callback for any + -- param with an open file handle. + local name, value + for name, value in pairs(self.message.params) do + if type(value) == "table" then + while value.fd do + local data = value.fd:read(1024) + local eof = (not data or data == "") + + callback(value, data, eof) + + if eof then + value.fd:close() + value.fd = nil + end + end + end + end +end + +function Request._parse_input(self) + parse_message_body( + self.input, + self.message, + self.filehandler + ) + self.parsed_input = true +end + +function close() + if not context.eoh then + context.eoh = true + coroutine.yield(3) + end + + if not context.closed then + context.closed = true + coroutine.yield(5) + end +end + +function content() + return context.request:content() +end + +function formvalue(name, noparse) + return context.request:formvalue(name, noparse) +end + +function formvaluetable(prefix) + return context.request:formvaluetable(prefix) +end + +function getcookie(name) + return context.request:getcookie(name) +end + +-- or the environment table itself. +function getenv(name) + return context.request:getenv(name) +end + +function setfilehandler(callback) + return context.request:setfilehandler(callback) +end + +function header(key, value) + if not context.headers then + context.headers = {} + end + context.headers[key:lower()] = value + coroutine.yield(2, key, value) +end + +function prepare_content(mime) + if not context.headers or not context.headers["content-type"] then + if mime == "application/xhtml+xml" then + if not getenv("HTTP_ACCEPT") or + not getenv("HTTP_ACCEPT"):find("application/xhtml+xml", nil, true) then + mime = "text/html; charset=UTF-8" + end + header("Vary", "Accept") + end + header("Content-Type", mime) + end +end + +function source() + return context.request.input +end + +function status(code, message) + code = code or 200 + message = message or "OK" + context.status = code + coroutine.yield(1, code, message) +end + +-- This function is as a valid LTN12 sink. +-- If the content chunk is nil this function will automatically invoke close. +function write(content, src_err) + if not content then + if src_err then + error(src_err) + else + close() + end + return true + elseif #content == 0 then + return true + else + if not context.eoh then + if not context.status then + status() + end + if not context.headers or not context.headers["content-type"] then + header("Content-Type", "text/html; charset=utf-8") + end + if not context.headers["cache-control"] then + header("Cache-Control", "no-cache") + header("Expires", "0") + end + if not context.headers["x-frame-options"] then + header("X-Frame-Options", "SAMEORIGIN") + end + if not context.headers["x-xss-protection"] then + header("X-XSS-Protection", "1; mode=block") + end + if not context.headers["x-content-type-options"] then + header("X-Content-Type-Options", "nosniff") + end + + context.eoh = true + coroutine.yield(3) + end + coroutine.yield(4, content) + return true + end +end + +function splice(fd, size) + coroutine.yield(6, fd, size) +end + +function redirect(url) + if url == "" then url = "/" end + status(302, "Found") + header("Location", url) + close() +end + +function build_querystring(q) + local s, n, k, v = {}, 1, nil, nil + + for k, v in pairs(q) do + s[n+0] = (n == 1) and "?" or "&" + s[n+1] = util.urlencode(k) + s[n+2] = "=" + s[n+3] = util.urlencode(v) + n = n + 4 + end + + return table.concat(s, "") +end + +urldecode = util.urldecode + +urlencode = util.urlencode + +function write_json(x) + util.serialize_json(x, write) +end + +-- from given url or string. Returns a table with urldecoded values. +-- Simple parameters are stored as string values associated with the parameter +-- name within the table. Parameters with multiple values are stored as array +-- containing the corresponding values. +function urldecode_params(url, tbl) + local parser, name + local params = tbl or { } + + parser = lhttp.urlencoded_parser(function (what, buffer, length) + if what == parser.TUPLE then + name, value = nil, nil + elseif what == parser.NAME then + name = lhttp.urldecode(buffer) + elseif what == parser.VALUE and name then + params[name] = lhttp.urldecode(buffer) or "" + end + + return true + end) + + if parser then + parser:parse((url or ""):match("[^?]*$")) + parser:parse(nil) + end + + return params +end + +-- separated by "&". Tables are encoded as parameters with multiple values by +-- repeating the parameter name with each value. +function urlencode_params(tbl) + local k, v + local n, enc = 1, {} + for k, v in pairs(tbl) do + if type(v) == "table" then + local i, v2 + for i, v2 in ipairs(v) do + if enc[1] then + enc[n] = "&" + n = n + 1 + end + + enc[n+0] = lhttp.urlencode(k) + enc[n+1] = "=" + enc[n+2] = lhttp.urlencode(v2) + n = n + 3 + end + else + if enc[1] then + enc[n] = "&" + n = n + 1 + end + + enc[n+0] = lhttp.urlencode(k) + enc[n+1] = "=" + enc[n+2] = lhttp.urlencode(v) + n = n + 3 + end + end + + return table.concat(enc, "") +end + +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table within the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +-- If an optional file callback function is given then it is fed with the +-- file contents chunk by chunk and only the extracted file name is stored +-- within the params table. The callback function will be called subsequently +-- with three arguments: +-- o Table containing decoded (name, file) and raw (headers) mime header data +-- o String value containing a chunk of the file data +-- o Boolean which indicates whether the current chunk is the last one (eof) +function mimedecode_message_body(src, msg, file_cb) + local parser, header, field + local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil) + + parser, err = lhttp.multipart_parser(msg.env.CONTENT_TYPE, function (what, buffer, length) + if what == parser.PART_INIT then + field = { } + + elseif what == parser.HEADER_NAME then + header = buffer:lower() + + elseif what == parser.HEADER_VALUE and header then + if header:lower() == "content-disposition" and + lhttp.header_attribute(buffer, nil) == "form-data" + then + field.name = lhttp.header_attribute(buffer, "name") + field.file = lhttp.header_attribute(buffer, "filename") + field[1] = field.file + end + + if field.headers then + field.headers[header] = buffer + else + field.headers = { [header] = buffer } + end + + elseif what == parser.PART_BEGIN then + return not field.file + + elseif what == parser.PART_DATA and field.name and length > 0 then + if field.file then + if file_cb then + file_cb(field, buffer, false) + msg.params[field.name] = msg.params[field.name] or field + else + if not field.fd then + field.fd = nixio.mkstemp(field.name) + end + + if field.fd then + field.fd:write(buffer) + msg.params[field.name] = msg.params[field.name] or field + end + end + else + field.value = buffer + end + + elseif what == parser.PART_END and field.name then + if field.file and msg.params[field.name] then + if file_cb then + file_cb(field, "", true) + elseif field.fd then + field.fd:seek(0, "set") + end + else + local val = msg.params[field.name] + + if type(val) == "table" then + val[#val+1] = field.value or "" + elseif val ~= nil then + msg.params[field.name] = { val, field.value or "" } + else + msg.params[field.name] = field.value or "" + end + end + + field = nil + + elseif what == parser.ERROR then + err = buffer + end + + return true + end, HTTP_MAX_CONTENT) + + return ltn12.pump.all(src, function (chunk) + len = len + (chunk and #chunk or 0) + + if maxlen and len > maxlen + 2 then + return nil, "Message body size exceeds Content-Length" + end + + if not parser or not parser:parse(chunk) then + return nil, err + end + + return true + end) +end + +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table within the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +function urldecode_message_body(src, msg) + local err, name, value, parser + local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil) + + parser = lhttp.urlencoded_parser(function (what, buffer, length) + if what == parser.TUPLE then + name, value = nil, nil + elseif what == parser.NAME then + name = lhttp.urldecode(buffer, lhttp.DECODE_PLUS) + elseif what == parser.VALUE and name then + local val = msg.params[name] + + if type(val) == "table" then + val[#val+1] = lhttp.urldecode(buffer, lhttp.DECODE_PLUS) or "" + elseif val ~= nil then + msg.params[name] = { val, lhttp.urldecode(buffer, lhttp.DECODE_PLUS) or "" } + else + msg.params[name] = lhttp.urldecode(buffer, lhttp.DECODE_PLUS) or "" + end + elseif what == parser.ERROR then + err = buffer + end + + return true + end, HTTP_MAX_CONTENT) + + return ltn12.pump.all(src, function (chunk) + len = len + (chunk and #chunk or 0) + + if maxlen and len > maxlen + 2 then + return nil, "Message body size exceeds Content-Length" + elseif len > HTTP_MAX_CONTENT then + return nil, "Message body size exceeds maximum allowed length" + end + + if not parser or not parser:parse(chunk) then + return nil, err + end + + return true + end) +end + +-- This function will examine the Content-Type within the given message object +-- to select the appropriate content decoder. +-- Currently the application/x-www-urlencoded and application/form-data +-- mime types are supported. If the encountered content encoding can't be +-- handled then the whole message body will be stored unaltered as "content" +-- property within the given message object. +function parse_message_body(src, msg, filecb) + if msg.env.CONTENT_LENGTH or msg.env.REQUEST_METHOD == "POST" then + local ctype = lhttp.header_attribute(msg.env.CONTENT_TYPE, nil) + + -- Is it multipart/mime ? + if ctype == "multipart/form-data" then + return mimedecode_message_body(src, msg, filecb) + + -- Is it application/x-www-form-urlencoded ? + elseif ctype == "application/x-www-form-urlencoded" then + return urldecode_message_body(src, msg) + + end + + -- Unhandled encoding + -- If a file callback is given then feed it chunk by chunk, else + -- store whole buffer in message.content + local sink + + -- If we have a file callback then feed it + if type(filecb) == "function" then + local meta = { + name = "raw", + encoding = msg.env.CONTENT_TYPE + } + sink = function( chunk ) + if chunk then + return filecb(meta, chunk, false) + else + return filecb(meta, nil, true) + end + end + -- ... else append to .content + else + msg.content = "" + msg.content_length = 0 + + sink = function( chunk ) + if chunk then + if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then + msg.content = msg.content .. chunk + msg.content_length = msg.content_length + #chunk + return true + else + return nil, "POST data exceeds maximum allowed length" + end + end + return true + end + end + + -- Pump data... + while true do + local ok, err = ltn12.pump.step( src, sink ) + + if not ok and err then + return nil, err + elseif not ok then -- eof + return true + end + end + + return true + end + + return false +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.luac new file mode 100644 index 000000000000..c67366e63b6b Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.lua new file mode 100644 index 000000000000..323912b65022 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.lua @@ -0,0 +1,55 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local tparser = require "luci.template.parser" +local util = require "luci.util" +local tostring = tostring + +module "luci.i18n" + +i18ndir = util.libpath() .. "/i18n/" +context = util.threadlocal() +default = "en" + + +function setlanguage(lang) + local code, subcode = lang:match("^([A-Za-z][A-Za-z])[%-_]([A-Za-z][A-Za-z])$") + if not (code and subcode) then + subcode = lang:match("^([A-Za-z][A-Za-z])$") + if not subcode then + return nil + end + end + + context.parent = code and code:lower() + context.lang = context.parent and context.parent.."-"..subcode:lower() or subcode:lower() + + if tparser.load_catalog(context.lang, i18ndir) and + tparser.change_catalog(context.lang) + then + return context.lang + + elseif context.parent then + if tparser.load_catalog(context.parent, i18ndir) and + tparser.change_catalog(context.parent) + then + return context.parent + end + end + + return nil +end + +function translate(key) + return tparser.translate(key) or key +end + +function translatef(key, ...) + return tostring(translate(key)):format(...) +end + +function dump() + local rv = {} + tparser.get_translations(function(k, v) rv[k] = v end) + return rv +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.luac new file mode 100644 index 000000000000..14387da7ab32 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.lua new file mode 100644 index 000000000000..3a7268ccaef6 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.lua @@ -0,0 +1,316 @@ +--[[ +LuaSocket 2.0.2 license +Copyright � 2004-2007 Diego Nehab + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +]]-- +--[[ + Changes made by LuCI project: + * Renamed to luci.ltn12 to avoid collisions with luasocket + * Added inline documentation +]]-- +----------------------------------------------------------------------------- +-- LTN12 - Filters, sources, sinks and pumps. +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id$ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local table = require("table") +local base = _G + +-- See http://lua-users.org/wiki/FiltersSourcesAndSinks for design concepts +module("luci.ltn12") + +filter = {} +source = {} +sink = {} +pump = {} + +-- 2048 seems to be better in windows... +BLOCKSIZE = 2048 +_VERSION = "LTN12 1.0.1" + +----------------------------------------------------------------------------- +-- Filter stuff +----------------------------------------------------------------------------- + + +-- by passing it each chunk and updating a context between calls. +function filter.cycle(low, ctx, extra) + base.assert(low) + return function(chunk) + local ret + ret, ctx = low(ctx, chunk, extra) + return ret + end +end + +-- (thanks to Wim Couwenberg) +function filter.chain(...) + local n = table.getn(arg) + local top, index = 1, 1 + local retry = "" + return function(chunk) + retry = chunk and retry + while true do + if index == top then + chunk = arg[index](chunk) + if chunk == "" or top == n then return chunk + elseif chunk then index = index + 1 + else + top = top+1 + index = top + end + else + chunk = arg[index](chunk or "") + if chunk == "" then + index = index - 1 + chunk = retry + elseif chunk then + if index == n then return chunk + else index = index + 1 end + else base.error("filter returned inappropriate nil") end + end + end + end +end + +----------------------------------------------------------------------------- +-- Source stuff +----------------------------------------------------------------------------- + + +-- create an empty source +local function empty() + return nil +end + +function source.empty() + return empty +end + +function source.error(err) + return function() + return nil, err + end +end + +function source.file(handle, io_err) + if handle then + return function() + local chunk = handle:read(BLOCKSIZE) + if chunk and chunk:len() == 0 then chunk = nil end + if not chunk then handle:close() end + return chunk + end + else return source.error(io_err or "unable to open file") end +end + +function source.simplify(src) + base.assert(src) + return function() + local chunk, err_or_new = src() + src = err_or_new or src + if not chunk then return nil, err_or_new + else return chunk end + end +end + +function source.string(s) + if s then + local i = 1 + return function() + local chunk = string.sub(s, i, i+BLOCKSIZE-1) + i = i + BLOCKSIZE + if chunk ~= "" then return chunk + else return nil end + end + else return source.empty() end +end + +function source.rewind(src) + base.assert(src) + local t = {} + return function(chunk) + if not chunk then + chunk = table.remove(t) + if not chunk then return src() + else return chunk end + else + t[#t+1] = chunk + end + end +end + +function source.chain(src, f) + base.assert(src and f) + local last_in, last_out = "", "" + local state = "feeding" + local err + return function() + if not last_out then + base.error('source is empty!', 2) + end + while true do + if state == "feeding" then + last_in, err = src() + if err then return nil, err end + last_out = f(last_in) + if not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + elseif last_out ~= "" then + state = "eating" + if last_in then last_in = "" end + return last_out + end + else + last_out = f(last_in) + if last_out == "" then + if last_in == "" then + state = "feeding" + else + base.error('filter returned ""') + end + elseif not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + else + return last_out + end + end + end + end +end + +-- Sources will be used one after the other, as if they were concatenated +-- (thanks to Wim Couwenberg) +function source.cat(...) + local src = table.remove(arg, 1) + return function() + while src do + local chunk, err = src() + if chunk then return chunk end + if err then return nil, err end + src = table.remove(arg, 1) + end + end +end + +----------------------------------------------------------------------------- +-- Sink stuff +----------------------------------------------------------------------------- + + +function sink.table(t) + t = t or {} + local f = function(chunk, err) + if chunk then t[#t+1] = chunk end + return 1 + end + return f, t +end + +function sink.simplify(snk) + base.assert(snk) + return function(chunk, err) + local ret, err_or_new = snk(chunk, err) + if not ret then return nil, err_or_new end + snk = err_or_new or snk + return 1 + end +end + +function sink.file(handle, io_err) + if handle then + return function(chunk, err) + if not chunk then + handle:close() + return 1 + else return handle:write(chunk) end + end + else return sink.error(io_err or "unable to open file") end +end + +-- creates a sink that discards data +local function null() + return 1 +end + +function sink.null() + return null +end + +function sink.error(err) + return function() + return nil, err + end +end + +function sink.chain(f, snk) + base.assert(f and snk) + return function(chunk, err) + if chunk ~= "" then + local filtered = f(chunk) + local done = chunk and "" + while true do + local ret, snkerr = snk(filtered, err) + if not ret then return nil, snkerr end + if filtered == done then return 1 end + filtered = f(done) + end + else return 1 end + end +end + +----------------------------------------------------------------------------- +-- Pump stuff +----------------------------------------------------------------------------- + + +function pump.step(src, snk) + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + if chunk and ret then return 1 + else return nil, src_err or snk_err end +end + +function pump.all(src, snk, step) + base.assert(src and snk) + step = step or pump.step + while true do + local ret, err = step(src, snk) + if not ret then + if err then return nil, err + else return 1 end + end + end +end + diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.luac new file mode 100644 index 000000000000..21912d15e384 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.lua new file mode 100755 index 000000000000..a23236e1161e --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.lua @@ -0,0 +1,12 @@ + +m = Map("hwnat", translate("Hardware NAT Acceleration"), + translate("The Hardware NAT Acceleration designed for reducing cpu loading")) + +s = m:section(TypedSection, "hwnat", "HWNAT") +s.addremove=false +s.anonymous = true +enable = s:option(Flag,"enabled",translate("Enable")) + +m:section(SimpleSection).template = "admin_mtk/hwnat_status" + +return m \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.luac new file mode 100644 index 000000000000..59f64ba7ab89 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.lua new file mode 100755 index 000000000000..9008c74f3b7d --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.lua @@ -0,0 +1,122 @@ +require "luci.model.uci" +require "luci.sys" +local sec_name +local cursor = luci.model.uci.cursor() +cursor:foreach("ipsec", "remote", function(s) sec_name = s['.name'] end) +m = Map("ipsec", translate("IP Security")) + +s = m:section(TypedSection, "remote") +s.anonymous = true +o=s:option(Value, "localGatewayName", translate("Local Gateway Name")) +o.default = sec_name + +o=s:option(ListValue, "enabled", translate("IPSec VPN")) +o.widget="radio" +o.orientation = "horizontal" +o:value("0", "Disable") -- Key and value pairs +o:value("1", "Enable") + +tunnel = m:section(NamedSection, "TUNNEL") +o=tunnel:option(Value, "local_subnet", translate("Local Group Subnet")) +o.rmempty= false +o.datatype = 'ip4addr' + +s = m:section(TypedSection, "remote") +s.anonymous = true +o=s:option(Value, "gateway", translate("Remote Gateway IP Address")) +o.rmempty= false +o.datatype = 'ip4addr' + +tunnel = m:section(NamedSection, "TUNNEL") +o=tunnel:option(Value, "remote_subnet", translate("Remote Group Subnet")) +o.rmempty= false +o.datatype = 'ip4addr' + +s = m:section(TypedSection, "remote") +s.anonymous = true +p = s:option(ListValue, "authentication_method",translate"Keying Mode") +p:value("psk", "PSK") + +o=s:option(Value, "pre_shared_key", translate("Pre-shared Key")) +o.rmempty= false + +tunnel = m:section(NamedSection, "TUNNEL") +x = tunnel:option(ListValue, "mode",translate"Auto Mode") +x:value("add", "Add") +x:value("route", "Route") +x:value("start", "Start") +x:value("ignore", "Ignore") + + +d = tunnel:option(ListValue, "keyexchange",translate"Key Exchange") +d:value("ikev2", "ikev2") +d.default = "ikev2" + +c = tunnel:option(Value, "ikelifetime",translate"IKE Lifetime") +c.default = "10800" + +e = tunnel:option(Value, "lifetime",translate"Key Lifetime") +e.default = "3600" + +z = m:section(NamedSection, "phase_1_settings", "crypto_proposal", "Phase 1 Settings") +z.addremove = false + +q = z:option(ListValue, "encryption_algorithm",translate"Encryption") +q:value("aes128", "AES128") +q:value("aes192", "AES192") +q:value("aes256", "AES256") +q.default = "aes128" + +w = z:option(ListValue, "hash_algorithm",translate"Authentication") +w:value("sha1", "SHA1") +w:value("sha256", "SHA256") +w.default = "sha1" + +r = z:option(ListValue, "dh_group",translate"Group") +r:value("modp768", "modp768") +r:value("modp1024", "modp1024") +r:value("modp1536", "modp1536") +r.default = "modp768" + +j = m:section(NamedSection, "phase_2_settings", "crypto_proposal", "Phase 2 Settings") +l = j:option(ListValue, "encryption_algorithm",translate"Encryption") +l:value("aes128", "AES128") +l:value("aes192", "AES192") +l:value("aes256", "AES256") +l.default = "aes128" + +t = j:option(ListValue, "hash_algorithm",translate"Authentication") +t:value("sha1", "SHA1") +t:value("sha256", "SHA256") +t.default = "sha1" + +u = j:option(ListValue, "dh_group",translate"Group") +u:value("modp768", "modp768") +u:value("modp1024", "modp1024") +u:value("modp1536", "modp1536") +u.default = "modp768" + +con = m:section(TypedSection, "remote") +con.anonymous = true +status = con:option(Value, "vpn_status", translate"Status") +status.default = "Disconnected/Command not found" + +function m.on_commit(Map) + + local cur = luci.model.uci.cursor() + CBI_PREFIX = "cbid.ipsec." + local org_sec_name,new_sec_name + cur:foreach("ipsec", "remote", function(s) org_sec_name = s['.name'] end) + + local new_sec_name = luci.http.formvalue(CBI_PREFIX ..org_sec_name.. ".localGatewayName") + + if(new_sec_name ~= org_sec_name) then + --cur:rename('ipsec',org_sec_name,new_sec_name) + cur:save('ipsec') + cur:commit('ipsec') + end +end + +m:section(SimpleSection).template = "admin_mtk/mtk_ipsec_view" + +return m diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.luac new file mode 100644 index 000000000000..842eee35f0fe Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.lua new file mode 100644 index 000000000000..816f6f20538a --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.lua @@ -0,0 +1,508 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local os = require "os" +local util = require "luci.util" +local table = require "table" + + +local setmetatable, rawget, rawset = setmetatable, rawget, rawset +local require, getmetatable, assert = require, getmetatable, assert +local error, pairs, ipairs, select = error, pairs, ipairs, select +local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack + +-- The typical workflow for UCI is: Get a cursor instance from the +-- cursor factory, modify data (via Cursor.add, Cursor.delete, etc.), +-- save the changes to the staging area via Cursor.save and finally +-- Cursor.commit the data to the actual config files. +-- LuCI then needs to Cursor.apply the changes so daemons etc. are +-- reloaded. +module "luci.model.uci" + +local ERRSTR = { + "Invalid command", + "Invalid argument", + "Method not found", + "Entry not found", + "No data", + "Permission denied", + "Timeout", + "Not supported", + "Unknown error", + "Connection failed" +} + +local session_id = nil + +local function call(cmd, args) + if type(args) == "table" and session_id then + args.ubus_rpc_session = session_id + end + return util.ubus("uci", cmd, args) +end + + +function cursor() + return _M +end + +function cursor_state() + return _M +end + +function substate(self) + return self +end + + +function get_confdir(self) + return "/etc/config" +end + +function get_savedir(self) + return "/tmp/.uci" +end + +function get_session_id(self) + return session_id +end + +function set_confdir(self, directory) + return false +end + +function set_savedir(self, directory) + return false +end + +function set_session_id(self, id) + session_id = id + return true +end + + +function load(self, config) + return true +end + +function save(self, config) + return true +end + +function unload(self, config) + return true +end + + +function changes(self, config) + local rv, err = call("changes", { config = config }) + + if type(rv) == "table" and type(rv.changes) == "table" then + return rv.changes + elseif err then + return nil, ERRSTR[err] + else + return { } + end +end + + +function revert(self, config) + local _, err = call("revert", { config = config }) + return (err == nil), ERRSTR[err] +end + +function commit(self, config) + local _, err = call("commit", { config = config }) + return (err == nil), ERRSTR[err] +end + +function apply(self, rollback) + local _, err + + if rollback then + local sys = require "luci.sys" + local conf = require "luci.config" + local timeout = tonumber(conf and conf.apply and conf.apply.rollback or 90) or 0 + + _, err = call("apply", { + timeout = (timeout > 90) and timeout or 90, + rollback = true + }) + + if not err then + local now = os.time() + local token = sys.uniqueid(16) + + util.ubus("session", "set", { + ubus_rpc_session = "00000000000000000000000000000000", + values = { + rollback = { + token = token, + session = session_id, + timeout = now + timeout + } + } + }) + + return token + end + else + _, err = call("changes", {}) + + if not err then + if type(_) == "table" and type(_.changes) == "table" then + local k, v + for k, v in pairs(_.changes) do + _, err = call("commit", { config = k }) + if err then + break + end + end + end + end + + if not err then + _, err = call("apply", { rollback = false }) + end + end + + return (err == nil), ERRSTR[err] +end + +function confirm(self, token) + local is_pending, time_remaining, rollback_sid, rollback_token = self:rollback_pending() + + if is_pending then + if token ~= rollback_token then + return false, "Permission denied" + end + + local _, err = util.ubus("uci", "confirm", { + ubus_rpc_session = rollback_sid + }) + + if not err then + util.ubus("session", "set", { + ubus_rpc_session = "00000000000000000000000000000000", + values = { rollback = {} } + }) + end + + return (err == nil), ERRSTR[err] + end + + return false, "No data" +end + +function rollback(self) + local is_pending, time_remaining, rollback_sid = self:rollback_pending() + + if is_pending then + local _, err = util.ubus("uci", "rollback", { + ubus_rpc_session = rollback_sid + }) + + if not err then + util.ubus("session", "set", { + ubus_rpc_session = "00000000000000000000000000000000", + values = { rollback = {} } + }) + end + + return (err == nil), ERRSTR[err] + end + + return false, "No data" +end + +function rollback_pending(self) + local rv, err = util.ubus("session", "get", { + ubus_rpc_session = "00000000000000000000000000000000", + keys = { "rollback" } + }) + + local now = os.time() + + if type(rv) == "table" and + type(rv.values) == "table" and + type(rv.values.rollback) == "table" and + type(rv.values.rollback.token) == "string" and + type(rv.values.rollback.session) == "string" and + type(rv.values.rollback.timeout) == "number" and + rv.values.rollback.timeout > now + then + return true, + rv.values.rollback.timeout - now, + rv.values.rollback.session, + rv.values.rollback.token + end + + return false, ERRSTR[err] +end + + +function foreach(self, config, stype, callback) + if type(callback) == "function" then + local rv, err = call("get", { + config = config, + type = stype + }) + + if type(rv) == "table" and type(rv.values) == "table" then + local sections = { } + local res = false + local index = 1 + + local _, section + for _, section in pairs(rv.values) do + section[".index"] = section[".index"] or index + sections[index] = section + index = index + 1 + end + + table.sort(sections, function(a, b) + return a[".index"] < b[".index"] + end) + + for _, section in ipairs(sections) do + local continue = callback(section) + res = true + if continue == false then + break + end + end + return res + else + return false, ERRSTR[err] or "No data" + end + else + return false, "Invalid argument" + end +end + +local function _get(self, operation, config, section, option) + if section == nil then + return nil + elseif type(option) == "string" and option:byte(1) ~= 46 then + local rv, err = call(operation, { + config = config, + section = section, + option = option + }) + + if type(rv) == "table" then + return rv.value or nil + elseif err then + return false, ERRSTR[err] + else + return nil + end + elseif option == nil then + local values = self:get_all(config, section) + if values then + return values[".type"], values[".name"] + else + return nil + end + else + return false, "Invalid argument" + end +end + +function get(self, ...) + return _get(self, "get", ...) +end + +function get_state(self, ...) + return _get(self, "state", ...) +end + +function get_all(self, config, section) + local rv, err = call("get", { + config = config, + section = section + }) + + if type(rv) == "table" and type(rv.values) == "table" then + return rv.values + elseif err then + return false, ERRSTR[err] + else + return nil + end +end + +function get_bool(self, ...) + local val = self:get(...) + return (val == "1" or val == "true" or val == "yes" or val == "on") +end + +function get_first(self, config, stype, option, default) + local rv = default + + self:foreach(config, stype, function(s) + local val = not option and s[".name"] or s[option] + + if type(default) == "number" then + val = tonumber(val) + elseif type(default) == "boolean" then + val = (val == "1" or val == "true" or + val == "yes" or val == "on") + end + + if val ~= nil then + rv = val + return false + end + end) + + return rv +end + +function get_list(self, config, section, option) + if config and section and option then + local val = self:get(config, section, option) + return (type(val) == "table" and val or { val }) + end + return { } +end + + +function section(self, config, stype, name, values) + local rv, err = call("add", { + config = config, + type = stype, + name = name, + values = values + }) + + if type(rv) == "table" then + return rv.section + elseif err then + return false, ERRSTR[err] + else + return nil + end +end + + +function add(self, config, stype) + return self:section(config, stype) +end + +function set(self, config, section, option, ...) + if select('#', ...) == 0 then + local sname, err = self:section(config, option, section) + return (not not sname), err + else + local _, err = call("set", { + config = config, + section = section, + values = { [option] = select(1, ...) } + }) + return (err == nil), ERRSTR[err] + end +end + +function set_list(self, config, section, option, value) + if section == nil or option == nil then + return false + elseif value == nil or (type(value) == "table" and #value == 0) then + return self:delete(config, section, option) + elseif type(value) == "table" then + return self:set(config, section, option, value) + else + return self:set(config, section, option, { value }) + end +end + +function tset(self, config, section, values) + local _, err = call("set", { + config = config, + section = section, + values = values + }) + return (err == nil), ERRSTR[err] +end + +function reorder(self, config, section, index) + local sections + + if type(section) == "string" and type(index) == "number" then + local pos = 0 + + sections = { } + + self:foreach(config, nil, function(s) + if pos == index then + pos = pos + 1 + end + + if s[".name"] ~= section then + pos = pos + 1 + sections[pos] = s[".name"] + else + sections[index + 1] = section + end + end) + elseif type(section) == "table" then + sections = section + else + return false, "Invalid argument" + end + + local _, err = call("order", { + config = config, + sections = sections + }) + + return (err == nil), ERRSTR[err] +end + + +function delete(self, config, section, option) + local _, err = call("delete", { + config = config, + section = section, + option = option + }) + return (err == nil), ERRSTR[err] +end + +function delete_all(self, config, stype, comparator) + local _, err + if type(comparator) == "table" then + _, err = call("delete", { + config = config, + type = stype, + match = comparator + }) + elseif type(comparator) == "function" then + local rv = call("get", { + config = config, + type = stype + }) + + if type(rv) == "table" and type(rv.values) == "table" then + local sname, section + for sname, section in pairs(rv.values) do + if comparator(section) then + _, err = call("delete", { + config = config, + section = sname + }) + end + end + end + elseif comparator == nil then + _, err = call("delete", { + config = config, + type = stype + }) + else + return false, "Invalid argument" + end + + return (err == nil), ERRSTR[err] +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.luac new file mode 100644 index 000000000000..43f9825df9d6 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.lua new file mode 100644 index 000000000000..400db4710d37 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.lua @@ -0,0 +1,73 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +exectime = os.clock() +module("luci.sgi.cgi", package.seeall) +local ltn12 = require("luci.ltn12") +require("nixio.util") +require("luci.http") +require("luci.sys") +require("luci.dispatcher") + +-- Limited source to avoid endless blocking +local function limitsource(handle, limit) + limit = limit or 0 + local BLOCKSIZE = ltn12.BLOCKSIZE + + return function() + if limit < 1 then + handle:close() + return nil + else + local read = (limit > BLOCKSIZE) and BLOCKSIZE or limit + limit = limit - read + + local chunk = handle:read(read) + if not chunk then handle:close() end + return chunk + end + end +end + +function run() + local r = luci.http.Request( + luci.sys.getenv(), + limitsource(io.stdin, tonumber(luci.sys.getenv("CONTENT_LENGTH"))), + ltn12.sink.file(io.stderr) + ) + + local x = coroutine.create(luci.dispatcher.httpdispatch) + local hcache = "" + local active = true + + while coroutine.status(x) ~= "dead" do + local res, id, data1, data2 = coroutine.resume(x, r) + + if not res then + print("Status: 500 Internal Server Error") + print("Content-Type: text/plain\n") + print(id) + break; + end + + if active then + if id == 1 then + io.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n") + elseif id == 2 then + hcache = hcache .. data1 .. ": " .. data2 .. "\r\n" + elseif id == 3 then + io.write(hcache) + io.write("\r\n") + elseif id == 4 then + io.write(tostring(data1 or "")) + elseif id == 5 then + io.flush() + io.close() + active = false + elseif id == 6 then + data1:copyz(nixio.stdout, data2) + data1:close() + end + end + end +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.luac new file mode 100644 index 000000000000..6f6a81f439a4 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.lua new file mode 100644 index 000000000000..4cd3649c62d8 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.lua @@ -0,0 +1,99 @@ +-- Copyright 2010 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +require "nixio.util" +require "luci.http" +require "luci.sys" +require "luci.dispatcher" +require "luci.ltn12" + +function handle_request(env) + exectime = os.clock() + local renv = { + CONTENT_LENGTH = env.CONTENT_LENGTH, + CONTENT_TYPE = env.CONTENT_TYPE, + REQUEST_METHOD = env.REQUEST_METHOD, + REQUEST_URI = env.REQUEST_URI, + PATH_INFO = env.PATH_INFO, + SCRIPT_NAME = env.SCRIPT_NAME:gsub("/+$", ""), + SCRIPT_FILENAME = env.SCRIPT_NAME, + SERVER_PROTOCOL = env.SERVER_PROTOCOL, + QUERY_STRING = env.QUERY_STRING, + DOCUMENT_ROOT = env.DOCUMENT_ROOT, + HTTPS = env.HTTPS, + REDIRECT_STATUS = env.REDIRECT_STATUS, + REMOTE_ADDR = env.REMOTE_ADDR, + REMOTE_NAME = env.REMOTE_NAME, + REMOTE_PORT = env.REMOTE_PORT, + REMOTE_USER = env.REMOTE_USER, + SERVER_ADDR = env.SERVER_ADDR, + SERVER_NAME = env.SERVER_NAME, + SERVER_PORT = env.SERVER_PORT + } + + local k, v + for k, v in pairs(env.headers) do + k = k:upper():gsub("%-", "_") + renv["HTTP_" .. k] = v + end + + local len = tonumber(env.CONTENT_LENGTH) or 0 + local function recv() + if len > 0 then + local rlen, rbuf = uhttpd.recv(4096) + if rlen >= 0 then + len = len - rlen + return rbuf + end + end + return nil + end + + local send = uhttpd.send + + local req = luci.http.Request( + renv, recv, luci.ltn12.sink.file(io.stderr) + ) + + + local x = coroutine.create(luci.dispatcher.httpdispatch) + local hcache = { } + local active = true + + while coroutine.status(x) ~= "dead" do + local res, id, data1, data2 = coroutine.resume(x, req) + + if not res then + send("Status: 500 Internal Server Error\r\n") + send("Content-Type: text/plain\r\n\r\n") + send(tostring(id)) + break + end + + if active then + if id == 1 then + send("Status: ") + send(tostring(data1)) + send(" ") + send(tostring(data2)) + send("\r\n") + elseif id == 2 then + hcache[data1] = data2 + elseif id == 3 then + for k, v in pairs(hcache) do + send(tostring(k)) + send(": ") + send(tostring(v)) + send("\r\n") + end + send("\r\n") + elseif id == 4 then + send(tostring(data1 or "")) + elseif id == 5 then + active = false + elseif id == 6 then + data1:copyz(nixio.stdout, data2) + end + end + end +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.luac new file mode 100644 index 000000000000..624406e5c8c5 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.lua new file mode 100644 index 000000000000..a73598113780 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.lua @@ -0,0 +1,6 @@ +-- Copyright 2009 Steven Barth +-- Copyright 2009 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +module("luci.store", util.threadlocal) diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.luac new file mode 100644 index 000000000000..980e2628ef03 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.lua new file mode 100644 index 000000000000..e6eb762e48b1 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.lua @@ -0,0 +1,615 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local io = require "io" +local os = require "os" +local table = require "table" +local nixio = require "nixio" +local fs = require "nixio.fs" +local uci = require "luci.model.uci" + +local luci = {} +luci.util = require "luci.util" +luci.ip = require "luci.ip" + +local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack = + tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack + + +module "luci.sys" + +function call(...) + return os.execute(...) / 256 +end + +exec = luci.util.exec + +-- containing the whole environment is returned otherwise this function returns +-- the corresponding string value for the given name or nil if no such variable +-- exists. +getenv = nixio.getenv + +function hostname(newname) + if type(newname) == "string" and #newname > 0 then + fs.writefile( "/proc/sys/kernel/hostname", newname ) + return newname + else + return nixio.uname().nodename + end +end + +function httpget(url, stream, target) + if not target then + local source = stream and io.popen or luci.util.exec + return source("wget -qO- %s" % luci.util.shellquote(url)) + else + return os.execute("wget -qO %s %s" % + {luci.util.shellquote(target), luci.util.shellquote(url)}) + end +end + +function reboot() + return os.execute("reboot >/dev/null 2>&1") +end + +function syslog() + return luci.util.exec("logread") +end + +function dmesg() + return luci.util.exec("dmesg") +end + +function uniqueid(bytes) + local rand = fs.readfile("/dev/urandom", bytes) + return rand and nixio.bin.hexlify(rand) +end + +function uptime() + return nixio.sysinfo().uptime +end + + +net = {} + +local function _nethints(what, callback) + local _, k, e, mac, ip, name, duid, iaid + local cur = uci.cursor() + local ifn = { } + local hosts = { } + local lookup = { } + + local function _add(i, ...) + local k = select(i, ...) + if k then + if not hosts[k] then hosts[k] = { } end + hosts[k][1] = select(1, ...) or hosts[k][1] + hosts[k][2] = select(2, ...) or hosts[k][2] + hosts[k][3] = select(3, ...) or hosts[k][3] + hosts[k][4] = select(4, ...) or hosts[k][4] + end + end + + luci.ip.neighbors(nil, function(neigh) + if neigh.mac and neigh.family == 4 then + _add(what, neigh.mac:string(), neigh.dest:string(), nil, nil) + elseif neigh.mac and neigh.family == 6 then + _add(what, neigh.mac:string(), nil, neigh.dest:string(), nil) + end + end) + + if fs.access("/etc/ethers") then + for e in io.lines("/etc/ethers") do + mac, name = e:match("^([a-fA-F0-9:-]+)%s+(%S+)") + mac = luci.ip.checkmac(mac) + if mac and name then + if luci.ip.checkip4(name) then + _add(what, mac, name, nil, nil) + else + _add(what, mac, nil, nil, name) + end + end + end + end + + cur:foreach("dhcp", "dnsmasq", + function(s) + if s.leasefile and fs.access(s.leasefile) then + for e in io.lines(s.leasefile) do + mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)") + mac = luci.ip.checkmac(mac) + if mac and ip then + _add(what, mac, ip, nil, name ~= "*" and name) + end + end + end + end + ) + + cur:foreach("dhcp", "odhcpd", + function(s) + if type(s.leasefile) == "string" and fs.access(s.leasefile) then + for e in io.lines(s.leasefile) do + duid, iaid, name, _, ip = e:match("^# %S+ (%S+) (%S+) (%S+) (-?%d+) %S+ %S+ ([0-9a-f:.]+)/[0-9]+") + mac = net.duid_to_mac(duid) + if mac then + if ip and iaid == "ipv4" then + _add(what, mac, ip, nil, name ~= "*" and name) + elseif ip then + _add(what, mac, nil, ip, name ~= "*" and name) + end + end + end + end + end + ) + + cur:foreach("dhcp", "host", + function(s) + for mac in luci.util.imatch(s.mac) do + mac = luci.ip.checkmac(mac) + if mac then + _add(what, mac, s.ip, nil, s.name) + end + end + end) + + for _, e in ipairs(nixio.getifaddrs()) do + if e.name ~= "lo" then + ifn[e.name] = ifn[e.name] or { } + if e.family == "packet" and e.addr and #e.addr == 17 then + ifn[e.name][1] = e.addr:upper() + elseif e.family == "inet" then + ifn[e.name][2] = e.addr + elseif e.family == "inet6" then + ifn[e.name][3] = e.addr + end + end + end + + for _, e in pairs(ifn) do + if e[what] and (e[2] or e[3]) then + _add(what, e[1], e[2], e[3], e[4]) + end + end + + for _, e in pairs(hosts) do + lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3]) + end + + if #lookup > 0 then + lookup = luci.util.ubus("network.rrdns", "lookup", { + addrs = lookup, + timeout = 250, + limit = 1000 + }) or { } + end + + for _, e in luci.util.kspairs(hosts) do + callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4]) + end +end + +-- Each entry contains the values in the following order: +-- [ "mac", "name" ] +function net.mac_hints(callback) + if callback then + _nethints(1, function(mac, v4, v6, name) + name = name or v4 + if name and name ~= mac then + callback(mac, name or v4) + end + end) + else + local rv = { } + _nethints(1, function(mac, v4, v6, name) + name = name or v4 + if name and name ~= mac then + rv[#rv+1] = { mac, name or v4 } + end + end) + return rv + end +end + +-- Each entry contains the values in the following order: +-- [ "ip", "name" ] +function net.ipv4_hints(callback) + if callback then + _nethints(2, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v4 then + callback(v4, name) + end + end) + else + local rv = { } + _nethints(2, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v4 then + rv[#rv+1] = { v4, name } + end + end) + return rv + end +end + +-- Each entry contains the values in the following order: +-- [ "ip", "name" ] +function net.ipv6_hints(callback) + if callback then + _nethints(3, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v6 then + callback(v6, name) + end + end) + else + local rv = { } + _nethints(3, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v6 then + rv[#rv+1] = { v6, name } + end + end) + return rv + end +end + +function net.host_hints(callback) + if callback then + _nethints(1, function(mac, v4, v6, name) + if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then + callback(mac, v4, v6, name) + end + end) + else + local rv = { } + _nethints(1, function(mac, v4, v6, name) + if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then + local e = { } + if v4 then e.ipv4 = v4 end + if v6 then e.ipv6 = v6 end + if name then e.name = name end + rv[mac] = e + end + end) + return rv + end +end + +function net.conntrack(callback) + local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack") + if not ok or not nfct then + return nil + end + + local line, connt = nil, (not callback) and { } + for line in nfct do + local fam, l3, l4, rest = + line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(.+)$") + + local timeout, tuples = rest:match("^(%d+) +(.+)$") + + if not tuples then + tuples = rest + end + + if fam and l3 and l4 and not tuples:match("^TIME_WAIT ") then + l4 = nixio.getprotobynumber(l4) + + local entry = { + bytes = 0, + packets = 0, + layer3 = fam, + layer4 = l4 and l4.name or "unknown", + timeout = tonumber(timeout, 10) + } + + local key, val + for key, val in tuples:gmatch("(%w+)=(%S+)") do + if key == "bytes" or key == "packets" then + entry[key] = entry[key] + tonumber(val, 10) + elseif key == "src" or key == "dst" then + if entry[key] == nil then + entry[key] = luci.ip.new(val):string() + end + elseif key == "sport" or key == "dport" then + if entry[key] == nil then + entry[key] = val + end + elseif val then + entry[key] = val + end + end + + if callback then + callback(entry) + else + connt[#connt+1] = entry + end + end + end + + return callback and true or connt +end + +function net.devices() + local devs = {} + local seen = {} + for k, v in ipairs(nixio.getifaddrs()) do + if v.name and not seen[v.name] then + seen[v.name] = true + devs[#devs+1] = v.name + end + end + return devs +end + +function net.duid_to_mac(duid) + local b1, b2, b3, b4, b5, b6 + + if type(duid) == "string" then + -- DUID-LLT / Ethernet + if #duid == 28 then + b1, b2, b3, b4, b5, b6 = duid:match("^00010001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)%x%x%x%x%x%x%x%x$") + + -- DUID-LL / Ethernet + elseif #duid == 20 then + b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$") + + -- DUID-LL / Ethernet (Without Header) + elseif #duid == 12 then + b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$") + end + end + + return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":")) +end + +process = {} + +function process.info(key) + local s = {uid = nixio.getuid(), gid = nixio.getgid()} + return not key and s or s[key] +end + +function process.list() + local data = {} + local k + local ps = luci.util.execi("/bin/busybox top -bn1") + + if not ps then + return + end + + for line in ps do + local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match( + "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][ 2 then + fd:close() + end +end + +function process.exec(command, stdout, stderr, nowait) + local out_r, out_w, err_r, err_w + if stdout then out_r, out_w = nixio.pipe() end + if stderr then err_r, err_w = nixio.pipe() end + + local pid = nixio.fork() + if pid == 0 then + nixio.chdir("/") + + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(out_w or null, nixio.stdout) + nixio.dup(err_w or null, nixio.stderr) + nixio.dup(null, nixio.stdin) + xclose(out_w) + xclose(out_r) + xclose(err_w) + xclose(err_r) + xclose(null) + end + + nixio.exec(unpack(command)) + os.exit(-1) + end + + local _, pfds, rv = nil, {}, { code = -1, pid = pid } + + xclose(out_w) + xclose(err_w) + + if out_r then + pfds[#pfds+1] = { + fd = out_r, + cb = type(stdout) == "function" and stdout, + name = "stdout", + events = nixio.poll_flags("in", "err", "hup") + } + end + + if err_r then + pfds[#pfds+1] = { + fd = err_r, + cb = type(stderr) == "function" and stderr, + name = "stderr", + events = nixio.poll_flags("in", "err", "hup") + } + end + + while #pfds > 0 do + local nfds, err = nixio.poll(pfds, -1) + if not nfds and err ~= nixio.const.EINTR then + break + end + + local i + for i = #pfds, 1, -1 do + local rfd = pfds[i] + if rfd.revents > 0 then + local chunk, err = rfd.fd:read(4096) + if chunk and #chunk > 0 then + if rfd.cb then + rfd.cb(chunk) + else + rfd.buf = rfd.buf or {} + rfd.buf[#rfd.buf + 1] = chunk + end + else + table.remove(pfds, i) + if rfd.buf then + rv[rfd.name] = table.concat(rfd.buf, "") + end + rfd.fd:close() + end + end + end + end + + if not nowait then + _, _, rv.code = nixio.waitpid(pid) + end + + return rv +end + + +user = {} + +-- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" } +user.getuser = nixio.getpw + +function user.getpasswd(username) + local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username) + local pwh = pwe and (pwe.pwdp or pwe.passwd) + if not pwh or #pwh < 1 then + return nil, pwe + else + return pwh, pwe + end +end + +function user.checkpasswd(username, pass) + local pwh, pwe = user.getpasswd(username) + if pwe then + return (pwh == nil or nixio.crypt(pass, pwh) == pwh) + end + return false +end + +function user.setpasswd(username, password) + return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{ + luci.util.shellquote(password), + luci.util.shellquote(password), + luci.util.shellquote(username) + }) +end + + +wifi = {} + +function wifi.getiwinfo(ifname) + local ntm = require "luci.model.network" + + ntm.init() + + local wnet = ntm:get_wifinet(ifname) + if wnet and wnet.iwinfo then + return wnet.iwinfo + end + + local wdev = ntm:get_wifidev(ifname) + if wdev and wdev.iwinfo then + return wdev.iwinfo + end + + return { ifname = ifname } +end + + +init = {} +init.dir = "/etc/init.d/" + +function init.names() + local names = { } + for name in fs.glob(init.dir.."*") do + names[#names+1] = fs.basename(name) + end + return names +end + +function init.index(name) + name = fs.basename(name) + if fs.access(init.dir..name) then + return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null" + %{ init.dir, name }) + end +end + +local function init_action(action, name) + name = fs.basename(name) + if fs.access(init.dir..name) then + return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action }) + end +end + +function init.enabled(name) + return (init_action("enabled", name) == 0) +end + +function init.enable(name) + return (init_action("enable", name) == 0) +end + +function init.disable(name) + return (init_action("disable", name) == 0) +end + +function init.start(name) + return (init_action("start", name) == 0) +end + +function init.stop(name) + return (init_action("stop", name) == 0) +end + +function init.restart(name) + return (init_action("restart", name) == 0) +end + +function init.reload(name) + return (init_action("reload", name) == 0) +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.luac new file mode 100644 index 000000000000..51176d875b64 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.lua new file mode 100644 index 000000000000..aa054a246f3b --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.lua @@ -0,0 +1,19 @@ +-- Licensed to the public under the Apache License 2.0. + +local setmetatable, require, rawget, rawset = setmetatable, require, rawget, rawset + +module "luci.sys.zoneinfo" + +setmetatable(_M, { + __index = function(t, k) + if k == "TZ" and not rawget(t, k) then + local m = require "luci.sys.zoneinfo.tzdata" + rawset(t, k, rawget(m, k)) + elseif k == "OFFSET" and not rawget(t, k) then + local m = require "luci.sys.zoneinfo.tzoffset" + rawset(t, k, rawget(m, k)) + end + + return rawget(t, k) + end +}) diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.luac new file mode 100644 index 000000000000..dcb9040981a0 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua new file mode 100644 index 000000000000..a3edbf5cb49a --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua @@ -0,0 +1,457 @@ +-- Licensed to the public under the Apache License 2.0. + +module "luci.sys.zoneinfo.tzdata" + +TZ = { + { 'Africa/Abidjan', 'GMT0' }, + { 'Africa/Accra', 'GMT0' }, + { 'Africa/Addis Ababa', 'EAT-3' }, + { 'Africa/Algiers', 'CET-1' }, + { 'Africa/Asmara', 'EAT-3' }, + { 'Africa/Bamako', 'GMT0' }, + { 'Africa/Bangui', 'WAT-1' }, + { 'Africa/Banjul', 'GMT0' }, + { 'Africa/Bissau', 'GMT0' }, + { 'Africa/Blantyre', 'CAT-2' }, + { 'Africa/Brazzaville', 'WAT-1' }, + { 'Africa/Bujumbura', 'CAT-2' }, + { 'Africa/Cairo', 'EET-2' }, + { 'Africa/Casablanca', '<+01>-1' }, + { 'Africa/Ceuta', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Africa/Conakry', 'GMT0' }, + { 'Africa/Dakar', 'GMT0' }, + { 'Africa/Dar es Salaam', 'EAT-3' }, + { 'Africa/Djibouti', 'EAT-3' }, + { 'Africa/Douala', 'WAT-1' }, + { 'Africa/El Aaiun', '<+01>-1' }, + { 'Africa/Freetown', 'GMT0' }, + { 'Africa/Gaborone', 'CAT-2' }, + { 'Africa/Harare', 'CAT-2' }, + { 'Africa/Johannesburg', 'SAST-2' }, + { 'Africa/Juba', 'CAT-2' }, + { 'Africa/Kampala', 'EAT-3' }, + { 'Africa/Khartoum', 'CAT-2' }, + { 'Africa/Kigali', 'CAT-2' }, + { 'Africa/Kinshasa', 'WAT-1' }, + { 'Africa/Lagos', 'WAT-1' }, + { 'Africa/Libreville', 'WAT-1' }, + { 'Africa/Lome', 'GMT0' }, + { 'Africa/Luanda', 'WAT-1' }, + { 'Africa/Lubumbashi', 'CAT-2' }, + { 'Africa/Lusaka', 'CAT-2' }, + { 'Africa/Malabo', 'WAT-1' }, + { 'Africa/Maputo', 'CAT-2' }, + { 'Africa/Maseru', 'SAST-2' }, + { 'Africa/Mbabane', 'SAST-2' }, + { 'Africa/Mogadishu', 'EAT-3' }, + { 'Africa/Monrovia', 'GMT0' }, + { 'Africa/Nairobi', 'EAT-3' }, + { 'Africa/Ndjamena', 'WAT-1' }, + { 'Africa/Niamey', 'WAT-1' }, + { 'Africa/Nouakchott', 'GMT0' }, + { 'Africa/Ouagadougou', 'GMT0' }, + { 'Africa/Porto-Novo', 'WAT-1' }, + { 'Africa/Sao Tome', 'GMT0' }, + { 'Africa/Tripoli', 'EET-2' }, + { 'Africa/Tunis', 'CET-1' }, + { 'Africa/Windhoek', 'CAT-2' }, + { 'America/Adak', 'HST10HDT,M3.2.0,M11.1.0' }, + { 'America/Anchorage', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Anguilla', 'AST4' }, + { 'America/Antigua', 'AST4' }, + { 'America/Araguaina', '<-03>3' }, + { 'America/Argentina/Buenos Aires', '<-03>3' }, + { 'America/Argentina/Catamarca', '<-03>3' }, + { 'America/Argentina/Cordoba', '<-03>3' }, + { 'America/Argentina/Jujuy', '<-03>3' }, + { 'America/Argentina/La Rioja', '<-03>3' }, + { 'America/Argentina/Mendoza', '<-03>3' }, + { 'America/Argentina/Rio Gallegos', '<-03>3' }, + { 'America/Argentina/Salta', '<-03>3' }, + { 'America/Argentina/San Juan', '<-03>3' }, + { 'America/Argentina/San Luis', '<-03>3' }, + { 'America/Argentina/Tucuman', '<-03>3' }, + { 'America/Argentina/Ushuaia', '<-03>3' }, + { 'America/Aruba', 'AST4' }, + { 'America/Asuncion', '<-04>4<-03>,M10.1.0/0,M3.4.0/0' }, + { 'America/Atikokan', 'EST5' }, + { 'America/Bahia', '<-03>3' }, + { 'America/Bahia Banderas', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Barbados', 'AST4' }, + { 'America/Belem', '<-03>3' }, + { 'America/Belize', 'CST6' }, + { 'America/Blanc-Sablon', 'AST4' }, + { 'America/Boa Vista', '<-04>4' }, + { 'America/Bogota', '<-05>5' }, + { 'America/Boise', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Cambridge Bay', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Campo Grande', '<-04>4' }, + { 'America/Cancun', 'EST5' }, + { 'America/Caracas', '<-04>4' }, + { 'America/Cayenne', '<-03>3' }, + { 'America/Cayman', 'EST5' }, + { 'America/Chicago', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Chihuahua', 'MST7MDT,M4.1.0,M10.5.0' }, + { 'America/Costa Rica', 'CST6' }, + { 'America/Creston', 'MST7' }, + { 'America/Cuiaba', '<-04>4' }, + { 'America/Curacao', 'AST4' }, + { 'America/Danmarkshavn', 'GMT0' }, + { 'America/Dawson', 'MST7' }, + { 'America/Dawson Creek', 'MST7' }, + { 'America/Denver', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Detroit', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Dominica', 'AST4' }, + { 'America/Edmonton', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Eirunepe', '<-05>5' }, + { 'America/El Salvador', 'CST6' }, + { 'America/Fort Nelson', 'MST7' }, + { 'America/Fortaleza', '<-03>3' }, + { 'America/Glace Bay', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Goose Bay', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Grand Turk', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Grenada', 'AST4' }, + { 'America/Guadeloupe', 'AST4' }, + { 'America/Guatemala', 'CST6' }, + { 'America/Guayaquil', '<-05>5' }, + { 'America/Guyana', '<-04>4' }, + { 'America/Halifax', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Havana', 'CST5CDT,M3.2.0/0,M11.1.0/1' }, + { 'America/Hermosillo', 'MST7' }, + { 'America/Indiana/Indianapolis', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Knox', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Marengo', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Petersburg', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Tell City', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Vevay', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Vincennes', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Winamac', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Inuvik', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Iqaluit', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Jamaica', 'EST5' }, + { 'America/Juneau', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Kentucky/Louisville', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Kentucky/Monticello', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Kralendijk', 'AST4' }, + { 'America/La Paz', '<-04>4' }, + { 'America/Lima', '<-05>5' }, + { 'America/Los Angeles', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Lower Princes', 'AST4' }, + { 'America/Maceio', '<-03>3' }, + { 'America/Managua', 'CST6' }, + { 'America/Manaus', '<-04>4' }, + { 'America/Marigot', 'AST4' }, + { 'America/Martinique', 'AST4' }, + { 'America/Matamoros', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Mazatlan', 'MST7MDT,M4.1.0,M10.5.0' }, + { 'America/Menominee', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Merida', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Metlakatla', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Mexico City', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Miquelon', '<-03>3<-02>,M3.2.0,M11.1.0' }, + { 'America/Moncton', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Monterrey', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Montevideo', '<-03>3' }, + { 'America/Montserrat', 'AST4' }, + { 'America/Nassau', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/New York', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Nipigon', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Nome', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Noronha', '<-02>2' }, + { 'America/North Dakota/Beulah', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/North Dakota/Center', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/North Dakota/New Salem', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Nuuk', '<-03>3<-02>,M3.5.0/-2,M10.5.0/-1' }, + { 'America/Ojinaga', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Panama', 'EST5' }, + { 'America/Pangnirtung', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Paramaribo', '<-03>3' }, + { 'America/Phoenix', 'MST7' }, + { 'America/Port of Spain', 'AST4' }, + { 'America/Port-au-Prince', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Porto Velho', '<-04>4' }, + { 'America/Puerto Rico', 'AST4' }, + { 'America/Punta Arenas', '<-03>3' }, + { 'America/Rainy River', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Rankin Inlet', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Recife', '<-03>3' }, + { 'America/Regina', 'CST6' }, + { 'America/Resolute', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Rio Branco', '<-05>5' }, + { 'America/Santarem', '<-03>3' }, + { 'America/Santiago', '<-04>4<-03>,M9.1.6/24,M4.1.6/24' }, + { 'America/Santo Domingo', 'AST4' }, + { 'America/Sao Paulo', '<-03>3' }, + { 'America/Scoresbysund', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' }, + { 'America/Sitka', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/St Barthelemy', 'AST4' }, + { 'America/St Johns', 'NST3:30NDT,M3.2.0,M11.1.0' }, + { 'America/St Kitts', 'AST4' }, + { 'America/St Lucia', 'AST4' }, + { 'America/St Thomas', 'AST4' }, + { 'America/St Vincent', 'AST4' }, + { 'America/Swift Current', 'CST6' }, + { 'America/Tegucigalpa', 'CST6' }, + { 'America/Thule', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Thunder Bay', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Tijuana', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Toronto', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Tortola', 'AST4' }, + { 'America/Vancouver', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Whitehorse', 'MST7' }, + { 'America/Winnipeg', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Yakutat', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Yellowknife', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'Antarctica/Casey', '<+11>-11' }, + { 'Antarctica/Davis', '<+07>-7' }, + { 'Antarctica/DumontDUrville', '<+10>-10' }, + { 'Antarctica/Macquarie', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Antarctica/Mawson', '<+05>-5' }, + { 'Antarctica/McMurdo', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Antarctica/Palmer', '<-03>3' }, + { 'Antarctica/Rothera', '<-03>3' }, + { 'Antarctica/Syowa', '<+03>-3' }, + { 'Antarctica/Troll', '<+00>0<+02>-2,M3.5.0/1,M10.5.0/3' }, + { 'Antarctica/Vostok', '<+06>-6' }, + { 'Arctic/Longyearbyen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Asia/Aden', '<+03>-3' }, + { 'Asia/Almaty', '<+06>-6' }, + { 'Asia/Amman', 'EET-2EEST,M2.5.4/24,M10.5.5/1' }, + { 'Asia/Anadyr', '<+12>-12' }, + { 'Asia/Aqtau', '<+05>-5' }, + { 'Asia/Aqtobe', '<+05>-5' }, + { 'Asia/Ashgabat', '<+05>-5' }, + { 'Asia/Atyrau', '<+05>-5' }, + { 'Asia/Baghdad', '<+03>-3' }, + { 'Asia/Bahrain', '<+03>-3' }, + { 'Asia/Baku', '<+04>-4' }, + { 'Asia/Bangkok', '<+07>-7' }, + { 'Asia/Barnaul', '<+07>-7' }, + { 'Asia/Beirut', 'EET-2EEST,M3.5.0/0,M10.5.0/0' }, + { 'Asia/Bishkek', '<+06>-6' }, + { 'Asia/Brunei', '<+08>-8' }, + { 'Asia/Chita', '<+09>-9' }, + { 'Asia/Choibalsan', '<+08>-8' }, + { 'Asia/Colombo', '<+0530>-5:30' }, + { 'Asia/Damascus', 'EET-2EEST,M3.5.5/0,M10.5.5/0' }, + { 'Asia/Dhaka', '<+06>-6' }, + { 'Asia/Dili', '<+09>-9' }, + { 'Asia/Dubai', '<+04>-4' }, + { 'Asia/Dushanbe', '<+05>-5' }, + { 'Asia/Famagusta', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Asia/Gaza', 'EET-2EEST,M3.4.4/72,M10.4.4/25' }, + { 'Asia/Hebron', 'EET-2EEST,M3.4.4/72,M10.4.4/25' }, + { 'Asia/Ho Chi Minh', '<+07>-7' }, + { 'Asia/Hong Kong', 'HKT-8' }, + { 'Asia/Hovd', '<+07>-7' }, + { 'Asia/Irkutsk', '<+08>-8' }, + { 'Asia/Jakarta', 'WIB-7' }, + { 'Asia/Jayapura', 'WIT-9' }, + { 'Asia/Jerusalem', 'IST-2IDT,M3.4.4/26,M10.5.0' }, + { 'Asia/Kabul', '<+0430>-4:30' }, + { 'Asia/Kamchatka', '<+12>-12' }, + { 'Asia/Karachi', 'PKT-5' }, + { 'Asia/Kathmandu', '<+0545>-5:45' }, + { 'Asia/Khandyga', '<+09>-9' }, + { 'Asia/Kolkata', 'IST-5:30' }, + { 'Asia/Krasnoyarsk', '<+07>-7' }, + { 'Asia/Kuala Lumpur', '<+08>-8' }, + { 'Asia/Kuching', '<+08>-8' }, + { 'Asia/Kuwait', '<+03>-3' }, + { 'Asia/Macau', 'CST-8' }, + { 'Asia/Magadan', '<+11>-11' }, + { 'Asia/Makassar', 'WITA-8' }, + { 'Asia/Manila', 'PST-8' }, + { 'Asia/Muscat', '<+04>-4' }, + { 'Asia/Nicosia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Asia/Novokuznetsk', '<+07>-7' }, + { 'Asia/Novosibirsk', '<+07>-7' }, + { 'Asia/Omsk', '<+06>-6' }, + { 'Asia/Oral', '<+05>-5' }, + { 'Asia/Phnom Penh', '<+07>-7' }, + { 'Asia/Pontianak', 'WIB-7' }, + { 'Asia/Pyongyang', 'KST-9' }, + { 'Asia/Qatar', '<+03>-3' }, + { 'Asia/Qostanay', '<+06>-6' }, + { 'Asia/Qyzylorda', '<+05>-5' }, + { 'Asia/Riyadh', '<+03>-3' }, + { 'Asia/Sakhalin', '<+11>-11' }, + { 'Asia/Samarkand', '<+05>-5' }, + { 'Asia/Seoul', 'KST-9' }, + { 'Asia/Shanghai', 'CST-8' }, + { 'Asia/Singapore', '<+08>-8' }, + { 'Asia/Srednekolymsk', '<+11>-11' }, + { 'Asia/Taipei', 'CST-8' }, + { 'Asia/Tashkent', '<+05>-5' }, + { 'Asia/Tbilisi', '<+04>-4' }, + { 'Asia/Tehran', '<+0330>-3:30' }, + { 'Asia/Thimphu', '<+06>-6' }, + { 'Asia/Tokyo', 'JST-9' }, + { 'Asia/Tomsk', '<+07>-7' }, + { 'Asia/Ulaanbaatar', '<+08>-8' }, + { 'Asia/Urumqi', '<+06>-6' }, + { 'Asia/Ust-Nera', '<+10>-10' }, + { 'Asia/Vientiane', '<+07>-7' }, + { 'Asia/Vladivostok', '<+10>-10' }, + { 'Asia/Yakutsk', '<+09>-9' }, + { 'Asia/Yangon', '<+0630>-6:30' }, + { 'Asia/Yekaterinburg', '<+05>-5' }, + { 'Asia/Yerevan', '<+04>-4' }, + { 'Atlantic/Azores', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' }, + { 'Atlantic/Bermuda', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'Atlantic/Canary', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Atlantic/Cape Verde', '<-01>1' }, + { 'Atlantic/Faroe', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Atlantic/Madeira', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Atlantic/Reykjavik', 'GMT0' }, + { 'Atlantic/South Georgia', '<-02>2' }, + { 'Atlantic/St Helena', 'GMT0' }, + { 'Atlantic/Stanley', '<-03>3' }, + { 'Australia/Adelaide', 'ACST-9:30ACDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Brisbane', 'AEST-10' }, + { 'Australia/Broken Hill', 'ACST-9:30ACDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Darwin', 'ACST-9:30' }, + { 'Australia/Eucla', '<+0845>-8:45' }, + { 'Australia/Hobart', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Lindeman', 'AEST-10' }, + { 'Australia/Lord Howe', '<+1030>-10:30<+11>-11,M10.1.0,M4.1.0' }, + { 'Australia/Melbourne', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Perth', 'AWST-8' }, + { 'Australia/Sydney', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Etc/GMT', 'GMT0' }, + { 'Etc/GMT+1', '<-01>1' }, + { 'Etc/GMT+10', '<-10>10' }, + { 'Etc/GMT+11', '<-11>11' }, + { 'Etc/GMT+12', '<-12>12' }, + { 'Etc/GMT+2', '<-02>2' }, + { 'Etc/GMT+3', '<-03>3' }, + { 'Etc/GMT+4', '<-04>4' }, + { 'Etc/GMT+5', '<-05>5' }, + { 'Etc/GMT+6', '<-06>6' }, + { 'Etc/GMT+7', '<-07>7' }, + { 'Etc/GMT+8', '<-08>8' }, + { 'Etc/GMT+9', '<-09>9' }, + { 'Etc/GMT-1', '<+01>-1' }, + { 'Etc/GMT-10', '<+10>-10' }, + { 'Etc/GMT-11', '<+11>-11' }, + { 'Etc/GMT-12', '<+12>-12' }, + { 'Etc/GMT-13', '<+13>-13' }, + { 'Etc/GMT-14', '<+14>-14' }, + { 'Etc/GMT-2', '<+02>-2' }, + { 'Etc/GMT-3', '<+03>-3' }, + { 'Etc/GMT-4', '<+04>-4' }, + { 'Etc/GMT-5', '<+05>-5' }, + { 'Etc/GMT-6', '<+06>-6' }, + { 'Etc/GMT-7', '<+07>-7' }, + { 'Etc/GMT-8', '<+08>-8' }, + { 'Etc/GMT-9', '<+09>-9' }, + { 'Europe/Amsterdam', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Andorra', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Astrakhan', '<+04>-4' }, + { 'Europe/Athens', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Belgrade', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Berlin', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Bratislava', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Brussels', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Bucharest', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Budapest', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Busingen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Chisinau', 'EET-2EEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Copenhagen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Dublin', 'IST-1GMT0,M10.5.0,M3.5.0/1' }, + { 'Europe/Gibraltar', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Guernsey', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Helsinki', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Isle of Man', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Istanbul', '<+03>-3' }, + { 'Europe/Jersey', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Kaliningrad', 'EET-2' }, + { 'Europe/Kirov', '<+03>-3' }, + { 'Europe/Kyiv', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Lisbon', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Europe/Ljubljana', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/London', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Luxembourg', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Madrid', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Malta', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Mariehamn', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Minsk', '<+03>-3' }, + { 'Europe/Monaco', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Moscow', 'MSK-3' }, + { 'Europe/Oslo', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Paris', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Podgorica', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Prague', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Riga', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Rome', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Samara', '<+04>-4' }, + { 'Europe/San Marino', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Sarajevo', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Saratov', '<+04>-4' }, + { 'Europe/Simferopol', 'MSK-3' }, + { 'Europe/Skopje', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Sofia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Stockholm', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Tallinn', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Tirane', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Ulyanovsk', '<+04>-4' }, + { 'Europe/Uzhgorod', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Vaduz', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Vatican', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Vienna', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Vilnius', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Volgograd', '<+03>-3' }, + { 'Europe/Warsaw', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Zagreb', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Zaporozhye', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Zurich', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Indian/Antananarivo', 'EAT-3' }, + { 'Indian/Chagos', '<+06>-6' }, + { 'Indian/Christmas', '<+07>-7' }, + { 'Indian/Cocos', '<+0630>-6:30' }, + { 'Indian/Comoro', 'EAT-3' }, + { 'Indian/Kerguelen', '<+05>-5' }, + { 'Indian/Mahe', '<+04>-4' }, + { 'Indian/Maldives', '<+05>-5' }, + { 'Indian/Mauritius', '<+04>-4' }, + { 'Indian/Mayotte', 'EAT-3' }, + { 'Indian/Reunion', '<+04>-4' }, + { 'Pacific/Apia', '<+13>-13' }, + { 'Pacific/Auckland', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Pacific/Bougainville', '<+11>-11' }, + { 'Pacific/Chatham', '<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45' }, + { 'Pacific/Chuuk', '<+10>-10' }, + { 'Pacific/Easter', '<-06>6<-05>,M9.1.6/22,M4.1.6/22' }, + { 'Pacific/Efate', '<+11>-11' }, + { 'Pacific/Fakaofo', '<+13>-13' }, + { 'Pacific/Fiji', '<+12>-12<+13>,M11.2.0,M1.2.3/99' }, + { 'Pacific/Funafuti', '<+12>-12' }, + { 'Pacific/Galapagos', '<-06>6' }, + { 'Pacific/Gambier', '<-09>9' }, + { 'Pacific/Guadalcanal', '<+11>-11' }, + { 'Pacific/Guam', 'ChST-10' }, + { 'Pacific/Honolulu', 'HST10' }, + { 'Pacific/Kanton', '<+13>-13' }, + { 'Pacific/Kiritimati', '<+14>-14' }, + { 'Pacific/Kosrae', '<+11>-11' }, + { 'Pacific/Kwajalein', '<+12>-12' }, + { 'Pacific/Majuro', '<+12>-12' }, + { 'Pacific/Marquesas', '<-0930>9:30' }, + { 'Pacific/Midway', 'SST11' }, + { 'Pacific/Nauru', '<+12>-12' }, + { 'Pacific/Niue', '<-11>11' }, + { 'Pacific/Norfolk', '<+11>-11<+12>,M10.1.0,M4.1.0/3' }, + { 'Pacific/Noumea', '<+11>-11' }, + { 'Pacific/Pago Pago', 'SST11' }, + { 'Pacific/Palau', '<+09>-9' }, + { 'Pacific/Pitcairn', '<-08>8' }, + { 'Pacific/Pohnpei', '<+11>-11' }, + { 'Pacific/Port Moresby', '<+10>-10' }, + { 'Pacific/Rarotonga', '<-10>10' }, + { 'Pacific/Saipan', 'ChST-10' }, + { 'Pacific/Tahiti', '<-10>10' }, + { 'Pacific/Tarawa', '<+12>-12' }, + { 'Pacific/Tongatapu', '<+13>-13' }, + { 'Pacific/Wake', '<+12>-12' }, + { 'Pacific/Wallis', '<+12>-12' }, +} diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac new file mode 100644 index 000000000000..5b9c75607146 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua new file mode 100644 index 000000000000..caee1d2c1c78 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua @@ -0,0 +1,46 @@ +-- Licensed to the public under the Apache License 2.0. + +module "luci.sys.zoneinfo.tzoffset" + +OFFSET = { + gmt = 0, -- GMT + eat = 10800, -- EAT + cet = 3600, -- CET + wat = 3600, -- WAT + cat = 7200, -- CAT + eet = 7200, -- EET + sast = 7200, -- SAST + hst = -36000, -- HST + hdt = -32400, -- HDT + akst = -32400, -- AKST + akdt = -28800, -- AKDT + ast = -14400, -- AST + est = -18000, -- EST + cst = -21600, -- CST + cdt = -18000, -- CDT + mst = -25200, -- MST + mdt = -21600, -- MDT + pst = -28800, -- PST + pdt = -25200, -- PDT + nst = -12600, -- NST + ndt = -9000, -- NDT + aest = 36000, -- AEST + aedt = 39600, -- AEDT + nzst = 43200, -- NZST + nzdt = 46800, -- NZDT + hkt = 28800, -- HKT + wib = 25200, -- WIB + wit = 32400, -- WIT + ist = 7200, -- IST + idt = 10800, -- IDT + pkt = 18000, -- PKT + wita = 28800, -- WITA + kst = 32400, -- KST + jst = 32400, -- JST + wet = 0, -- WET + acst = 34200, -- ACST + acdt = 37800, -- ACDT + awst = 28800, -- AWST + msk = 10800, -- MSK + sst = -39600, -- SST +} diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac new file mode 100644 index 000000000000..e261e6d5900f Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.lua new file mode 100644 index 000000000000..3955bd76f3a7 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.lua @@ -0,0 +1,100 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +local config = require "luci.config" +local tparser = require "luci.template.parser" + +local tostring, pairs, loadstring = tostring, pairs, loadstring +local setmetatable, loadfile = setmetatable, loadfile +local getfenv, setfenv, rawget = getfenv, setfenv, rawget +local assert, type, error = assert, type, error + +--- LuCI template library. +module "luci.template" + +config.template = config.template or {} +viewdir = config.template.viewdir or util.libpath() .. "/view" + + +-- Define the namespace for template modules +context = util.threadlocal() + +--- Render a certain template. +-- @param name Template name +-- @param scope Scope to assign to template (optional) +function render(name, scope) + return Template(name):render(scope or getfenv(2)) +end + +--- Render a template from a string. +-- @param template Template string +-- @param scope Scope to assign to template (optional) +function render_string(template, scope) + return Template(nil, template):render(scope or getfenv(2)) +end + + +-- Template class +Template = util.class() + +-- Shared template cache to store templates in to avoid unnecessary reloading +Template.cache = setmetatable({}, {__mode = "v"}) + + +-- Constructor - Reads and compiles the template on-demand +function Template.__init__(self, name, template) + if name then + self.template = self.cache[name] + self.name = name + else + self.name = "[string]" + end + + -- Create a new namespace for this template + self.viewns = context.viewns + + -- If we have a cached template, skip compiling and loading + if not self.template then + + -- Compile template + local err + local sourcefile + + if name then + sourcefile = viewdir .. "/" .. name .. ".htm" + self.template, _, err = tparser.parse(sourcefile) + else + sourcefile = "[string]" + self.template, _, err = tparser.parse_string(template) + end + + -- If we have no valid template throw error, otherwise cache the template + if not self.template then + error("Failed to load template '" .. self.name .. "'.\n" .. + "Error while parsing template '" .. sourcefile .. "':\n" .. + (err or "Unknown syntax error")) + elseif name then + self.cache[name] = self.template + end + end +end + + +-- Renders a template +function Template.render(self, scope) + scope = scope or getfenv(2) + + -- Put our predefined objects in the scope of the template + setfenv(self.template, setmetatable({}, {__index = + function(tbl, key) + return rawget(tbl, key) or self.viewns[key] or scope[key] + end})) + + -- Now finally render the thing + local stat, err = util.copcall(self.template) + if not stat then + error("Failed to execute template '" .. self.name .. "'.\n" .. + "A runtime error occurred: " .. tostring(err or "(nil)")) + end +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.luac new file mode 100644 index 000000000000..eeeacbdb7272 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.lua new file mode 100644 index 000000000000..89757917ff65 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.lua @@ -0,0 +1,782 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local io = require "io" +local math = require "math" +local table = require "table" +local debug = require "debug" +local ldebug = require "luci.debug" +local string = require "string" +local coroutine = require "coroutine" +local tparser = require "luci.template.parser" +local json = require "luci.jsonc" +local lhttp = require "lucihttp" + +local _ubus = require "ubus" +local _ubus_connection = nil + +local getmetatable, setmetatable = getmetatable, setmetatable +local rawget, rawset, unpack, select = rawget, rawset, unpack, select +local tostring, type, assert, error = tostring, type, assert, error +local ipairs, pairs, next, loadstring = ipairs, pairs, next, loadstring +local require, pcall, xpcall = require, pcall, xpcall +local collectgarbage, get_memory_limit = collectgarbage, get_memory_limit + +module "luci.util" + +-- +-- Pythonic string formatting extension +-- +getmetatable("").__mod = function(a, b) + local ok, res + + if not b then + return a + elseif type(b) == "table" then + local k, _ + for k, _ in pairs(b) do if type(b[k]) == "userdata" then b[k] = tostring(b[k]) end end + + ok, res = pcall(a.format, a, unpack(b)) + if not ok then + error(res, 2) + end + return res + else + if type(b) == "userdata" then b = tostring(b) end + + ok, res = pcall(a.format, a, b) + if not ok then + error(res, 2) + end + return res + end +end + + +-- +-- Class helper routines +-- + +-- Instantiates a class +local function _instantiate(class, ...) + local inst = setmetatable({}, {__index = class}) + + if inst.__init__ then + inst:__init__(...) + end + + return inst +end + +-- The class object can be instantiated by calling itself. +-- Any class functions or shared parameters can be attached to this object. +-- Attaching a table to the class object makes this table shared between +-- all instances of this class. For object parameters use the __init__ function. +-- Classes can inherit member functions and values from a base class. +-- Class can be instantiated by calling them. All parameters will be passed +-- to the __init__ function of this class - if such a function exists. +-- The __init__ function must be used to set any object parameters that are not shared +-- with other objects of this class. Any return values will be ignored. +function class(base) + return setmetatable({}, { + __call = _instantiate, + __index = base + }) +end + +function instanceof(object, class) + local meta = getmetatable(object) + while meta and meta.__index do + if meta.__index == class then + return true + end + meta = getmetatable(meta.__index) + end + return false +end + + +-- +-- Scope manipulation routines +-- + +coxpt = setmetatable({}, { __mode = "kv" }) + +local tl_meta = { + __mode = "k", + + __index = function(self, key) + local t = rawget(self, coxpt[coroutine.running()] + or coroutine.running() or 0) + return t and t[key] + end, + + __newindex = function(self, key, value) + local c = coxpt[coroutine.running()] or coroutine.running() or 0 + local r = rawget(self, c) + if not r then + rawset(self, c, { [key] = value }) + else + r[key] = value + end + end +} + +-- the current active coroutine. A thread local store is private a table object +-- whose values can't be accessed from outside of the running coroutine. +function threadlocal(tbl) + return setmetatable(tbl or {}, tl_meta) +end + + +-- +-- Debugging routines +-- + +function perror(obj) + return io.stderr:write(tostring(obj) .. "\n") +end + +function dumptable(t, maxdepth, i, seen) + i = i or 0 + seen = seen or setmetatable({}, {__mode="k"}) + + for k,v in pairs(t) do + perror(string.rep("\t", i) .. tostring(k) .. "\t" .. tostring(v)) + if type(v) == "table" and (not maxdepth or i < maxdepth) then + if not seen[v] then + seen[v] = true + dumptable(v, maxdepth, i+1, seen) + else + perror(string.rep("\t", i) .. "*** RECURSION ***") + end + end + end +end + + +-- +-- String and data manipulation routines +-- + +-- compatibility wrapper for xml.pcdata +function pcdata(value) + local xml = require "luci.xml" + + perror("luci.util.pcdata() has been replaced by luci.xml.pcdata() - Please update your code.") + return xml.pcdata(value) +end + +function urlencode(value) + if value ~= nil then + local str = tostring(value) + return lhttp.urlencode(str, lhttp.ENCODE_IF_NEEDED + lhttp.ENCODE_FULL) + or str + end + return nil +end + +function urldecode(value, decode_plus) + if value ~= nil then + local flag = decode_plus and lhttp.DECODE_PLUS or 0 + local str = tostring(value) + return lhttp.urldecode(str, lhttp.DECODE_IF_NEEDED + flag) + or str + end + return nil +end + +-- compatibility wrapper for xml.striptags +function striptags(value) + local xml = require "luci.xml" + + perror("luci.util.striptags() has been replaced by luci.xml.striptags() - Please update your code.") + return xml.striptags(value) +end + +function shellquote(value) + return string.format("'%s'", string.gsub(value or "", "'", "'\\''")) +end + +-- for bash, ash and similar shells single-quoted strings are taken +-- literally except for single quotes (which terminate the string) +-- (and the exception noted below for dash (-) at the start of a +-- command line parameter). +function shellsqescape(value) + local res + res, _ = string.gsub(value, "'", "'\\''") + return res +end + +-- bash, ash and other similar shells interpret a dash (-) at the start +-- of a command-line parameters as an option indicator regardless of +-- whether it is inside a single-quoted string. It must be backlash +-- escaped to resolve this. This requires in some funky special-case +-- handling. It may actually be a property of the getopt function +-- rather than the shell proper. +function shellstartsqescape(value) + res, _ = string.gsub(value, "^%-", "\\-") + return shellsqescape(res) +end + +-- containing the resulting substrings. The optional max parameter specifies +-- the number of bytes to process, regardless of the actual length of the given +-- string. The optional last parameter, regex, specifies whether the separator +-- sequence is interpreted as regular expression. +-- pattern as regular expression (optional, default is false) +function split(str, pat, max, regex) + pat = pat or "\n" + max = max or #str + + local t = {} + local c = 1 + + if #str == 0 then + return {""} + end + + if #pat == 0 then + return nil + end + + if max == 0 then + return str + end + + repeat + local s, e = str:find(pat, c, not regex) + max = max - 1 + if s and max < 0 then + t[#t+1] = str:sub(c) + else + t[#t+1] = str:sub(c, s and s - 1) + end + c = e and e + 1 or #str + 1 + until not s or max < 0 + + return t +end + +function trim(str) + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +function cmatch(str, pat) + local count = 0 + for _ in str:gmatch(pat) do count = count + 1 end + return count +end + +-- one token per invocation, the tokens are separated by whitespace. If the +-- input value is a table, it is transformed into a string first. A nil value +-- will result in a valid iterator which aborts with the first invocation. +function imatch(v) + if type(v) == "table" then + local k = nil + return function() + k = next(v, k) + return v[k] + end + + elseif type(v) == "number" or type(v) == "boolean" then + local x = true + return function() + if x then + x = false + return tostring(v) + end + end + + elseif type(v) == "userdata" or type(v) == "string" then + return tostring(v):gmatch("%S+") + end + + return function() end +end + +-- value or 0 if the unit is unknown. Upper- or lower case is irrelevant. +-- Recognized units are: +-- o "y" - one year (60*60*24*366) +-- o "m" - one month (60*60*24*31) +-- o "w" - one week (60*60*24*7) +-- o "d" - one day (60*60*24) +-- o "h" - one hour (60*60) +-- o "min" - one minute (60) +-- o "kb" - one kilobyte (1024) +-- o "mb" - one megabyte (1024*1024) +-- o "gb" - one gigabyte (1024*1024*1024) +-- o "kib" - one si kilobyte (1000) +-- o "mib" - one si megabyte (1000*1000) +-- o "gib" - one si gigabyte (1000*1000*1000) +function parse_units(ustr) + + local val = 0 + + -- unit map + local map = { + -- date stuff + y = 60 * 60 * 24 * 366, + m = 60 * 60 * 24 * 31, + w = 60 * 60 * 24 * 7, + d = 60 * 60 * 24, + h = 60 * 60, + min = 60, + + -- storage sizes + kb = 1024, + mb = 1024 * 1024, + gb = 1024 * 1024 * 1024, + + -- storage sizes (si) + kib = 1000, + mib = 1000 * 1000, + gib = 1000 * 1000 * 1000 + } + + -- parse input string + for spec in ustr:lower():gmatch("[0-9%.]+[a-zA-Z]*") do + + local num = spec:gsub("[^0-9%.]+$","") + local spn = spec:gsub("^[0-9%.]+", "") + + if map[spn] or map[spn:sub(1,1)] then + val = val + num * ( map[spn] or map[spn:sub(1,1)] ) + else + val = val + num + end + end + + + return val +end + +-- also register functions above in the central string class for convenience +string.split = split +string.trim = trim +string.cmatch = cmatch +string.parse_units = parse_units + + +function append(src, ...) + for i, a in ipairs({...}) do + if type(a) == "table" then + for j, v in ipairs(a) do + src[#src+1] = v + end + else + src[#src+1] = a + end + end + return src +end + +function combine(...) + return append({}, ...) +end + +function contains(table, value) + for k, v in pairs(table) do + if value == v then + return k + end + end + return false +end + +-- Both table are - in fact - merged together. +function update(t, updates) + for k, v in pairs(updates) do + t[k] = v + end +end + +function keys(t) + local keys = { } + if t then + for k, _ in kspairs(t) do + keys[#keys+1] = k + end + end + return keys +end + +function clone(object, deep) + local copy = {} + + for k, v in pairs(object) do + if deep and type(v) == "table" then + v = clone(v, deep) + end + copy[k] = v + end + + return setmetatable(copy, getmetatable(object)) +end + + +-- Serialize the contents of a table value. +function _serialize_table(t, seen) + assert(not seen[t], "Recursion detected.") + seen[t] = true + + local data = "" + local idata = "" + local ilen = 0 + + for k, v in pairs(t) do + if type(k) ~= "number" or k < 1 or math.floor(k) ~= k or ( k - #t ) > 3 then + k = serialize_data(k, seen) + v = serialize_data(v, seen) + data = data .. ( #data > 0 and ", " or "" ) .. + '[' .. k .. '] = ' .. v + elseif k > ilen then + ilen = k + end + end + + for i = 1, ilen do + local v = serialize_data(t[i], seen) + idata = idata .. ( #idata > 0 and ", " or "" ) .. v + end + + return idata .. ( #data > 0 and #idata > 0 and ", " or "" ) .. data +end + +-- with loadstring(). +function serialize_data(val, seen) + seen = seen or setmetatable({}, {__mode="k"}) + + if val == nil then + return "nil" + elseif type(val) == "number" then + return val + elseif type(val) == "string" then + return "%q" % val + elseif type(val) == "boolean" then + return val and "true" or "false" + elseif type(val) == "function" then + return "loadstring(%q)" % get_bytecode(val) + elseif type(val) == "table" then + return "{ " .. _serialize_table(val, seen) .. " }" + else + return '"[unhandled data type:' .. type(val) .. ']"' + end +end + +function restore_data(str) + return loadstring("return " .. str)() +end + + +-- +-- Byte code manipulation routines +-- + +-- will be stripped before it is returned. +function get_bytecode(val) + local code + + if type(val) == "function" then + code = string.dump(val) + else + code = string.dump( loadstring( "return " .. serialize_data(val) ) ) + end + + return code -- and strip_bytecode(code) +end + +-- numbers and debugging numbers will be discarded. Original version by +-- Peter Cawley (http://lua-users.org/lists/lua-l/2008-02/msg01158.html) +function strip_bytecode(code) + local version, format, endian, int, size, ins, num, lnum = code:byte(5, 12) + local subint + if endian == 1 then + subint = function(code, i, l) + local val = 0 + for n = l, 1, -1 do + val = val * 256 + code:byte(i + n - 1) + end + return val, i + l + end + else + subint = function(code, i, l) + local val = 0 + for n = 1, l, 1 do + val = val * 256 + code:byte(i + n - 1) + end + return val, i + l + end + end + + local function strip_function(code) + local count, offset = subint(code, 1, size) + local stripped = { string.rep("\0", size) } + local dirty = offset + count + offset = offset + count + int * 2 + 4 + offset = offset + int + subint(code, offset, int) * ins + count, offset = subint(code, offset, int) + for n = 1, count do + local t + t, offset = subint(code, offset, 1) + if t == 1 then + offset = offset + 1 + elseif t == 4 then + offset = offset + size + subint(code, offset, size) + elseif t == 3 then + offset = offset + num + elseif t == 254 or t == 9 then + offset = offset + lnum + end + end + count, offset = subint(code, offset, int) + stripped[#stripped+1] = code:sub(dirty, offset - 1) + for n = 1, count do + local proto, off = strip_function(code:sub(offset, -1)) + stripped[#stripped+1] = proto + offset = offset + off - 1 + end + offset = offset + subint(code, offset, int) * int + int + count, offset = subint(code, offset, int) + for n = 1, count do + offset = offset + subint(code, offset, size) + size + int * 2 + end + count, offset = subint(code, offset, int) + for n = 1, count do + offset = offset + subint(code, offset, size) + size + end + stripped[#stripped+1] = string.rep("\0", int * 3) + return table.concat(stripped), offset + end + + return code:sub(1,12) .. strip_function(code:sub(13,-1)) +end + + +-- +-- Sorting iterator functions +-- + +function _sortiter( t, f ) + local keys = { } + + local k, v + for k, v in pairs(t) do + keys[#keys+1] = k + end + + local _pos = 0 + + table.sort( keys, f ) + + return function() + _pos = _pos + 1 + if _pos <= #keys then + return keys[_pos], t[keys[_pos]], _pos + end + end +end + +-- the provided callback function. +function spairs(t,f) + return _sortiter( t, f ) +end + +-- The table pairs are sorted by key. +function kspairs(t) + return _sortiter( t ) +end + +-- The table pairs are sorted by value. +function vspairs(t) + return _sortiter( t, function (a,b) return t[a] < t[b] end ) +end + + +-- +-- System utility functions +-- + +function bigendian() + return string.byte(string.dump(function() end), 7) == 0 +end + +function exec(command) + local pp = io.popen(command) + local data = pp:read("*a") + pp:close() + + return data +end + +function execi(command) + local pp = io.popen(command) + + return pp and function() + local line = pp:read() + + if not line then + pp:close() + end + + return line + end +end + +-- Deprecated +function execl(command) + local pp = io.popen(command) + local line = "" + local data = {} + + while true do + line = pp:read() + if (line == nil) then break end + data[#data+1] = line + end + pp:close() + + return data +end + + +local ubus_codes = { + "INVALID_COMMAND", + "INVALID_ARGUMENT", + "METHOD_NOT_FOUND", + "NOT_FOUND", + "NO_DATA", + "PERMISSION_DENIED", + "TIMEOUT", + "NOT_SUPPORTED", + "UNKNOWN_ERROR", + "CONNECTION_FAILED" +} + +local function ubus_return(...) + if select('#', ...) == 2 then + local rv, err = select(1, ...), select(2, ...) + if rv == nil and type(err) == "number" then + return nil, err, ubus_codes[err] + end + end + + return ... +end + +function ubus(object, method, data, path, timeout) + if not _ubus_connection then + _ubus_connection = _ubus.connect(path, timeout) + assert(_ubus_connection, "Unable to establish ubus connection") + end + + if object and method then + if type(data) ~= "table" then + data = { } + end + return ubus_return(_ubus_connection:call(object, method, data)) + elseif object then + return _ubus_connection:signatures(object) + else + return _ubus_connection:objects() + end +end + +function serialize_json(x, cb) + local js = json.stringify(x) + if type(cb) == "function" then + cb(js) + else + return js + end +end + + +function libpath() + return require "nixio.fs".dirname(ldebug.__file__) +end + +function checklib(fullpathexe, wantedlib) + local fs = require "nixio.fs" + local haveldd = fs.access('/usr/bin/ldd') + local haveexe = fs.access(fullpathexe) + if not haveldd or not haveexe then + return false + end + local libs = exec(string.format("/usr/bin/ldd %s", shellquote(fullpathexe))) + if not libs then + return false + end + for k, v in ipairs(split(libs)) do + if v:find(wantedlib) then + return true + end + end + return false +end + +------------------------------------------------------------------------------- +-- Coroutine safe xpcall and pcall versions +-- +-- Encapsulates the protected calls with a coroutine based loop, so errors can +-- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines +-- yielding inside the call to pcall or xpcall. +-- +-- Authors: Roberto Ierusalimschy and Andre Carregal +-- Contributors: Thomas Harning Jr., Ignacio Burgueño, Fabio Mascarenhas +-- +-- Copyright 2005 - Kepler Project +-- +-- $Id: coxpcall.lua,v 1.13 2008/05/19 19:20:02 mascarenhas Exp $ +------------------------------------------------------------------------------- + +------------------------------------------------------------------------------- +-- Implements xpcall with coroutines +------------------------------------------------------------------------------- +local coromap = setmetatable({}, { __mode = "k" }) + +local function handleReturnValue(err, co, status, ...) + if not status then + return false, err(debug.traceback(co, (...)), ...) + end + if coroutine.status(co) == 'suspended' then + return performResume(err, co, coroutine.yield(...)) + else + return true, ... + end +end + +function performResume(err, co, ...) + return handleReturnValue(err, co, coroutine.resume(co, ...)) +end + +local function id(trace, ...) + return trace +end + +function coxpcall(f, err, ...) + local current = coroutine.running() + if not current then + if err == id then + return pcall(f, ...) + else + if select("#", ...) > 0 then + local oldf, params = f, { ... } + f = function() return oldf(unpack(params)) end + end + return xpcall(f, err) + end + else + local res, co = pcall(coroutine.create, f) + if not res then + local newf = function(...) return f(...) end + co = coroutine.create(newf) + end + coromap[co] = current + coxpt[co] = coxpt[current] or current or 0 + return performResume(err, co, ...) + end +end + +function copcall(f, ...) + return coxpcall(f, id, ...) +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.luac new file mode 100644 index 000000000000..9aca38e344a4 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.lua new file mode 100644 index 000000000000..79a4e6788e49 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.lua @@ -0,0 +1,20 @@ +local pcall, dofile, _G = pcall, dofile, _G + +module "luci.version" + +if pcall(dofile, "/etc/openwrt_release") and _G.DISTRIB_DESCRIPTION then + distname = "" + distversion = _G.DISTRIB_DESCRIPTION + if _G.DISTRIB_REVISION then + distrevision = _G.DISTRIB_REVISION + if not distversion:find(distrevision,1,true) then + distversion = distversion .. " " .. distrevision + end + end +else + distname = "OpenWrt" + distversion = "Development Snapshot" +end + +luciname = "LuCI master-snmp branch" +luciversion = "git-23.052.04749-af5524f" diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.luac new file mode 100644 index 000000000000..0b0869666d73 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.lua new file mode 100644 index 000000000000..30b37210bd83 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.lua @@ -0,0 +1,26 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local tparser = require "luci.template.parser" +local string = require "string" + +local tostring = tostring + +module "luci.xml" + +-- +-- String and data manipulation routines +-- + +function pcdata(value) + return value and tparser.pcdata(tostring(value)) +end + +function striptags(value) + return value and tparser.striptags(tostring(value)) +end + + +-- also register functions above in the central string class for convenience +string.pcdata = pcdata +string.striptags = striptags diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.luac new file mode 100644 index 000000000000..2047960288ee Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.lua new file mode 100644 index 000000000000..d3abac51cad5 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.lua @@ -0,0 +1,89 @@ +----------------------------------------------------------------------------- +-- MIME support for the Lua language. +-- Author: Diego Nehab +-- Conforming to RFCs 2045-2049 +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local ltn12 = require("ltn12") +local mime = require("mime.core") +local string = require("string") +local _M = mime + +-- encode, decode and wrap algorithm tables +local encodet, decodet, wrapt = {},{},{} + +_M.encodet = encodet +_M.decodet = decodet +_M.wrapt = wrapt + +-- creates a function that chooses a filter by name from a given table +local function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then + base.error("unknown key (" .. base.tostring(name) .. ")", 3) + else return f(opt1, opt2) end + end +end + +-- define the encoding filters +encodet['base64'] = function() + return ltn12.filter.cycle(_M.b64, "") +end + +encodet['quoted-printable'] = function(mode) + return ltn12.filter.cycle(_M.qp, "", + (mode == "binary") and "=0D=0A" or "\r\n") +end + +-- define the decoding filters +decodet['base64'] = function() + return ltn12.filter.cycle(_M.unb64, "") +end + +decodet['quoted-printable'] = function() + return ltn12.filter.cycle(_M.unqp, "") +end + +local function format(chunk) + if chunk then + if chunk == "" then return "''" + else return string.len(chunk) end + else return "nil" end +end + +-- define the line-wrap filters +wrapt['text'] = function(length) + length = length or 76 + return ltn12.filter.cycle(_M.wrp, length, length) +end +wrapt['base64'] = wrapt['text'] +wrapt['default'] = wrapt['text'] + +wrapt['quoted-printable'] = function() + return ltn12.filter.cycle(_M.qpwrp, 76, 76) +end + +-- function that choose the encoding, decoding or wrap algorithm +_M.encode = choose(encodet) +_M.decode = choose(decodet) +_M.wrap = choose(wrapt) + +-- define the end-of-line normalization filter +function _M.normalize(marker) + return ltn12.filter.cycle(_M.eol, 0, marker) +end + +-- high level stuffing filter +function _M.stuff() + return ltn12.filter.cycle(_M.dot, 2) +end + +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.luac new file mode 100644 index 000000000000..c16ac774c3ec Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.lua new file mode 100755 index 000000000000..6c50b46f7dc3 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.lua @@ -0,0 +1,2090 @@ +#!/usr/bin/env lua + +--[[ + * A lua library to manipulate mtk's wifi driver. used in luci-app-mtk. + * + * Copyright (C) 2016 Hua Shao + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] +require("datconf") +local ioctl_help = require "ioctl_helper" +local mtkwifi = {} +local logDisable = 1 +function debug_write(...) + -- luci.http.write(...) + if logDisable == 1 then + return + end + local syslog_msg = ""; + local ff = io.open("/tmp/mtkwifi", "a") + local nargs = select('#',...) + + + + for n=1, nargs do + local v = select(n,...) + if (type(v) == "string" or type(v) == "number") then + ff:write(v.." ") + syslog_msg = syslog_msg..v.." "; + elseif (type(v) == "boolean") then + if v then + ff:write("true ") + syslog_msg = syslog_msg.."true "; + else + ff:write("false ") + syslog_msg = syslog_msg.."false "; + end + elseif (type(v) == "nil") then + ff:write("nil ") + syslog_msg = syslog_msg.."nil "; + else + ff:write(" ") + syslog_msg = syslog_msg.." "; + end + end + ff:write("\n") + ff:close() + nixio.syslog("debug", syslog_msg) +end + +function mtkwifi.get_table_length(T) + local count = 0 + for _ in pairs(T) do + count = count + 1 + end + return count +end + +function mtkwifi.get_file_lines(fileName) + local fd = io.open(fileName, "r") + if not fd then return end + local content = fd:read("*all") + fd:close() + return mtkwifi.__lines(content) +end + +function mtkwifi.__split(s, delimiter) + if s == nil then s = "0" end + local result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match); + end + return result; +end + +function string:split(sep) + local sep, fields = sep or ":", {} + local pattern = string.format("([^%s]+)", sep) + self:gsub(pattern, function(c) fields[#fields+1] = c end) + return fields +end + +function mtkwifi.__trim(s) + if s then return (s:gsub("^%s*(.-)%s*$", "%1")) end +end + +function mtkwifi.__handleSpecialChars(s) + s = s:gsub("\\", "\\\\") + s = s:gsub("\"", "\\\"") + return s +end + +function mtkwifi.__spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + --[[ + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + -- table.sort(keys, order) + else + table.sort(keys) + end + ]] + table.sort(keys, order) + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + +function mtkwifi.__lines(str) + local t = {} + local function helper(line) table.insert(t, line) return "" end + helper((str:gsub("(.-)\r?\n", helper))) + return t +end + +function mtkwifi.__get_l1dat() + if not pcall(require, "l1dat_parser") then + return + end + + local parser = require("l1dat_parser") + local l1dat = parser.load_l1_profile(parser.L1_DAT_PATH) + + return l1dat, parser +end + +function mtkwifi.sleep(s) + local ntime = os.clock() + s + repeat until os.clock() > ntime +end + +function mtkwifi.deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[mtkwifi.deepcopy(orig_key)] = mtkwifi.deepcopy(orig_value) + end + setmetatable(copy, mtkwifi.deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end + +function mtkwifi.read_pipe(pipe) + local retry_count = 10 + local fp, txt, err + repeat -- fp:read() may return error, "Interrupted system call", and can be recovered by doing it again + fp = io.popen(pipe) + txt, err = fp:read("*a") + fp:close() + retry_count = retry_count - 1 + until err == nil or retry_count == 0 + return txt +end + +function mtkwifi.detect_triband() + local devs = mtkwifi.get_all_devs() + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1.DEV_RINDEX + local main_ifname + local bands = 0 + for _,dev in ipairs(devs) do + main_ifname = l1dat and l1dat[dridx][dev.devname].main_ifname or dbdc_prefix[mainidx][subidx].."0" + if mtkwifi.exists("/sys/class/net/"..main_ifname) then + bands = bands + 1 + end + end + return bands +end + +function mtkwifi.detect_first_card() + local devs = mtkwifi.get_all_devs() + local first_card_profile + + for i,dev in ipairs(devs) do + first_card_profile = dev.profile + if i == 1 then break end + end + + return first_card_profile +end + +function mtkwifi.load_profile(path, raw) + local cfgs = {} + + cfgobj = datconf.openfile(path) + if cfgobj then + cfgs = cfgobj:getall() + cfgobj:close() + elseif raw then + cfgs = datconf.parse(raw) + end + + return cfgs +end + +function mtkwifi.save_profile(cfgs, path) + + if not cfgs then + debug_write("configuration was empty, nothing saved") + return + end + + -- Keep a backup of last profile settings + -- if string.match(path, "([^/]+)\.dat") then + -- os.execute("cp -f "..path.." "..mtkwifi.__profile_previous_settings_path(path)) + -- end + local datobj = datconf.openfile(path) + datobj:merge(cfgs) + datobj:close(true) -- means close and commit + + if pcall(require, "mtknvram") then + local nvram = require("mtknvram") + local l1dat, l1 = mtkwifi.__get_l1dat() + local zone = l1 and l1.l1_path_to_zone(path) + + if pcall(require, "map_helper") and zone == "dev1" then + mtkwifi.save_easymesh_profile_to_nvram() + else + if not l1dat then + debug_write("save_profile: no l1dat", path) + nvram.nvram_save_profile(path) + else + if zone then + debug_write("save_profile:", path, zone) + nvram.nvram_save_profile(path, zone) + else + debug_write("save_profile:", path) + nvram.nvram_save_profile(path) + end + end + end + end + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.split_profile(path, path_2g, path_5g) + assert(path) + assert(path_2g) + assert(path_5g) + local cfgs = mtkwifi.load_profile(path) + local dirty = { + "Channel", + "WirelessMode", + "TxRate", + "WmmCapable", + "NoForwarding", + "HideSSID", + "IEEE8021X", + "PreAuth", + "AuthMode", + "EncrypType", + "RekeyMethod", + "RekeyInterval", + "PMKCachePeriod", + "DefaultKeyId", + "Key{n}Type", + "HT_EXTCHA", + "RADIUS_Server", + "RADIUS_Port", + } + local cfg5g = mtkwifi.deepcopy(cfgs) + for _,v in ipairs(dirty) do + cfg5g[v] = mtkwifi.token_get(cfgs[v], 1, 0) + assert(cfg5g[v]) + end + mtkwifi.save_profile(cfg5g, path_5g) + + local cfg2g = mtkwifi.deepcopy(cfgs) + for _,v in ipairs(dirty) do + cfg2g[v] = mtkwifi.token_get(cfgs[v], 1, 0) + assert(cfg2g[v]) + end + mtkwifi.save_profile(cfg2g, path_2g) +end + +function mtkwifi.merge_profile(path, path_2g, path_5g) + local cfg2g = mtkwifi.load_profile(path_2g) + local cfg5g = mtkwifi.load_profile(path_5g) + local dirty = { + "Channel", + "WirelessMode", + "TxRate", + "WmmCapable", + "NoForwarding", + "HideSSID", + "IEEE8021X", + "PreAuth", + "AuthMode", + "EncrypType", + "RekeyMethod", + "RekeyInterval", + "PMKCachePeriod", + "DefaultKeyId", + "Key{n}Type", + "HT_EXTCHA", + "RADIUS_Server", + "RADIUS_Port", + } + local cfgs = mtkwifi.deepcopy(cfg2g) + for _,v in dirty do + -- TODO + end + mtkwifi.save_profile(cfgs, path) +end + +-- update path1 by path2 +function mtkwifi.update_profile(path1, path2) + local cfg1 = datconf.openfile(path1) + local cfg2 = datconf.openfile(path2) + + cfg1:merge(cfg2:getall()) + cfg1:close(true) + cfg2:close() + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.__child_info_path() + local path = "/tmp/mtk/wifi/child_info.dat" + os.execute("mkdir -p /tmp/mtk/wifi") + return path +end + +function mtkwifi.__profile_previous_settings_path(profile) + assert(type(profile) == "string") + local bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)\.dat")..".last" + os.execute("mkdir -p /tmp/mtk/wifi") + return bak +end + +function mtkwifi.__profile_applied_settings_path(profile) + assert(type(profile) == "string") + local bak + if string.match(profile, "([^/]+)\.dat") then + os.execute("mkdir -p /tmp/mtk/wifi") + bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)\.dat")..".applied" + elseif string.match(profile, "([^/]+)\.txt") then + os.execute("mkdir -p /tmp/mtk/wifi") + bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)\.txt")..".applied" + elseif string.match(profile, "([^/]+)$") then + os.execute("mkdir -p /tmp/mtk/wifi") + bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)$")..".applied" + else + bak = "" + end + + return bak +end + +-- if path2 is not given, use backup of path1. +function mtkwifi.diff_profile(path1, path2) + assert(path1) + if not path2 then + path2 = mtkwifi.__profile_applied_settings_path(path1) + if not mtkwifi.exists(path2) then + return {} + end + end + assert(path2) + + local cfg1 + local cfg2 + local diff = {} + if path1 == mtkwifi.__easymesh_bss_cfgs_path() then + cfg1 = mtkwifi.get_file_lines(path1) or {} + cfg2 = mtkwifi.get_file_lines(path2) or {} + else + cfg1 = mtkwifi.load_profile(path1) or {} + cfg2 = mtkwifi.load_profile(path2) or {} + end + + for k,v in pairs(cfg1) do + if cfg2[k] ~= cfg1[k] then + diff[k] = {cfg1[k] or "", cfg2[k] or ""} + end + end + + for k,v in pairs(cfg2) do + if cfg2[k] ~= cfg1[k] then + diff[k] = {cfg1[k] or "", cfg2[k] or ""} + end + end + + return diff +end + +function mtkwifi.__fork_exec(command) + if type(command) ~= type("") or command == "" then + debug_write("__fork_exec : Incorrect command! Expected non-empty string type, got ",type(command)) + nixio.syslog("err", "__fork_exec : Incorrect command! Expected non-empty string type, got "..type(command)) + else + local nixio = require("nixio") + -- If nixio.exec() fails, then child process will be reaped automatically and + -- it will be achieved by ignoring SIGCHLD signal here in parent process! + if not nixio.signal(17,"ign") then + nixio.syslog("warning", "__fork_exec : Failed to set SIG_IGN for SIGCHLD!") + debug_write("__fork_exec : Failed to set SIG_IGN for SIGCHLD!") + end + local pid = nixio.fork() + if pid < 0 then + nixio.syslog("err", "__fork_exec : [Fork Failure] "..command) + debug_write("__fork_exec : [Fork Failure] "..command) + elseif pid == 0 then + -- change to root dir to flush out any opened directory streams of parent process. + nixio.chdir("/") + + -- As file descriptors are inherited by child process, all unused file descriptors must be closed. + -- Make stdin, out, err file descriptors point to /dev/null using dup2. + -- As a result, it will not corrupt stdin, out, err file descriptors of parent process. + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(null, nixio.stderr) + nixio.dup(null, nixio.stdout) + nixio.dup(null, nixio.stdin) + if null:fileno() > 2 then + null:close() + end + end + debug_write("__fork_exec : cmd = "..command) + -- replaces the child process image with the new process image generated by provided command + nixio.exec("/bin/sh", "-c", command) + os.exit(true) + end + end +end + +function mtkwifi.is_child_active() + local fd = io.open(mtkwifi.__child_info_path(), "r") + if not fd then + os.execute("rm -f "..mtkwifi.__child_info_path()) + return false + end + local content = fd:read("*all") + fd:close() + if not content then + os.execute("rm -f "..mtkwifi.__child_info_path()) + return false + end + local active_pid_list = {} + for _,pid in ipairs(mtkwifi.__lines(content)) do + pid = pid:match("CHILD_PID=%s*(%d+)%s*") + if pid then + if tonumber(mtkwifi.read_pipe("ps | grep -v grep | grep -cw "..pid)) == 1 then + table.insert(active_pid_list, pid) + end + end + end + if next(active_pid_list) ~= nil then + return true + else + os.execute("rm -f "..mtkwifi.__child_info_path()) + return false + end + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.__run_in_child_env(cbFn,...) + if type(cbFn) ~= "function" then + debug_write("__run_in_child_env : Function type expected, got ", type(cbFn)) + nixio.syslog("err", "__run_in_child_env : Function type expected, got "..type(cbFn)) + else + local unpack = unpack or table.unpack + local cbArgs = {...} + local nixio = require("nixio") + -- Let child process reap automatically! + if not nixio.signal(17,"ign") then + nixio.syslog("warning", "__run_in_child_env : Failed to set SIG_IGN for SIGCHLD!") + debug_write("__run_in_child_env : Failed to set SIG_IGN for SIGCHLD!") + end + local pid = nixio.fork() + if pid < 0 then + debug_write("__run_in_child_env : Fork failure") + nixio.syslog("err", "__run_in_child_env : Fork failure") + elseif pid == 0 then + -- Change to root dir to flush out any opened directory streams of parent process. + nixio.chdir("/") + + -- As file descriptors are inherited by child process, all unnecessary file descriptors must be closed. + -- Make stdin, out, err file descriptors point to /dev/null using dup2. + -- As a result, it will not corrupt stdin, out, err file descriptors of parent process. + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(null, nixio.stderr) + nixio.dup(null, nixio.stdout) + nixio.dup(null, nixio.stdin) + if null:fileno() > 2 then + null:close() + end + end + local fd = io.open(mtkwifi.__child_info_path(), "a") + if fd then + fd:write("CHILD_PID=",nixio.getpid(),"\n") + fd:close() + end + cbFn(unpack(cbArgs)) + os.exit(true) + end + end + os.execute("sync >/dev/null 2>&1") +end + +-- Mode 12 and 13 are only available for STAs. +local WirelessModeList = { + [0] = "B/G mixed", + [1] = "B only", + [2] = "A only", + -- [3] = "A/B/G mixed", + [4] = "G only", + -- [5] = "A/B/G/GN/AN mixed", + [6] = "N in 2.4G only", + [7] = "G/GN", -- i.e., no CCK mode + [8] = "A/N in 5 band", + [9] = "B/G/GN mode", + -- [10] = "A/AN/G/GN mode", --not support B mode + [11] = "only N in 5G band", + -- [12] = "B/G/GN/A/AN/AC mixed", + -- [13] = "G/GN/A/AN/AC mixed", -- no B mode + [14] = "A/AC/AN mixed", + [15] = "AC/AN mixed", --but no A mode + [16] = "HE_2G mode", --HE Wireless Mode + [17] = "HE_5G mode", --HE Wireless Mode + [18] = "HE_6G mode", --HE Wireless Mode +} + +local DevicePropertyMap = { + -- 2.4G + { + device="MT7622", + band={"0", "1", "4", "9"}, + isPowerBoostSupported=true, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7620", + band={"0", "1", "4", "9"}, + maxTxStream=2, + maxRxStream=2, + maxVif=8 + }, + + { + device="MT7628", + band={"0", "1", "4", "6", "7", "9"}, + maxTxStream=2, + maxRxStream=2, + maxVif=8, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7603", + band={"0", "1", "4", "6", "7", "9"}, + maxTxStream=2, + maxRxStream=2, + maxVif=8, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + -- 5G + { + device="MT7612", + band={"2", "8", "11", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + }, + + { + device="MT7662", + band={"2", "8", "11", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + }, + + -- Mix + { + device="MT7615", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=8 + }, + + { + device="MT7915", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161} + }, + + { + device="MT7916", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161}, + maxTxStream=2, + maxRxStream=2, + }, + + { + device="MT7981", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161}, + maxTxStream=2, + maxRxStream=2, + }, + + { + device="MT7986", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161}, + maxTxStream=4, + maxRxStream=4, + }, + + { + device="MT7663", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + invalidChBwList={160,161}, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7613", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + invalidChBwList={160,161}, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7626", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=3, + maxRxStream=3, + invalidChBwList={160,161}, + wdsBand="2.4G", + mimoBand="5G", + maxDBDCVif=8 + }, + + { + device="MT7629", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=3, + maxRxStream=3, + invalidChBwList={160,161}, + wdsBand="2.4G", + mimoBand="5G", + maxDBDCVif=8, + isMultiAPSupported=true + } +} + +mtkwifi.CountryRegionList_6G_All = { + {region=0, text="0: Ch1~233"}, + {region=1, text="1: Ch1~97"}, + {region=2, text="2: Ch101~117"}, + {region=3, text="3: Ch121~185"}, + {region=4, text="4: Ch189~233"}, + {region=5, text="5: Ch1~97"}, + {region=6, text="6: Ch1~97"}, + {region=7, text="7: Ch1~97, Ch101~109"}, +} + +mtkwifi.CountryRegionList_5G_All = { + {region=0, text="0: Ch36~64, Ch149~165"}, + {region=1, text="1: Ch36~64, Ch100~140"}, + {region=2, text="2: Ch36~64"}, + {region=3, text="3: Ch52~64, Ch149~161"}, + {region=4, text="4: Ch149~165"}, + {region=5, text="5: Ch149~161"}, + {region=6, text="6: Ch36~48"}, + {region=7, text="7: Ch36~64, Ch100~140, Ch149~165"}, + {region=8, text="8: Ch52~64"}, + {region=9, text="9: Ch36~64, Ch100~116, Ch132~140, Ch149~165"}, + {region=10, text="10: Ch36~48, Ch149~165"}, + {region=11, text="11: Ch36~64, Ch100~120, Ch149~161"}, + {region=12, text="12: Ch36~64, Ch100~144"}, + {region=13, text="13: Ch36~64, Ch100~144, Ch149~165"}, + {region=14, text="14: Ch36~64, Ch100~116, Ch132~144, Ch149~165"}, + {region=15, text="15: Ch149~173"}, + {region=16, text="16: Ch52~64, Ch149~165"}, + {region=17, text="17: Ch36~48, Ch149~161"}, + {region=18, text="18: Ch36~64, Ch100~116, Ch132~140"}, + {region=19, text="19: Ch56~64, Ch100~140, Ch149~161"}, + {region=20, text="20: Ch36~64, Ch100~124, Ch149~161"}, + {region=21, text="21: Ch36~64, Ch100~140, Ch149~161"}, + {region=22, text="22: Ch100~140"}, + {region=30, text="30: Ch36~48, Ch52~64, Ch100~140, Ch149~165"}, + {region=31, text="31: Ch52~64, Ch100~140, Ch149~165"}, + {region=32, text="32: Ch36~48, Ch52~64, Ch100~140, Ch149~161"}, + {region=33, text="33: Ch36~48, Ch52~64, Ch100~140"}, + {region=34, text="34: Ch36~48, Ch52~64, Ch149~165"}, + {region=35, text="35: Ch36~48, Ch52~64"}, + {region=36, text="36: Ch36~48, Ch100~140, Ch149~165"}, + {region=37, text="37: Ch36~48, Ch52~64, Ch149~165, Ch173"} +} + +mtkwifi.CountryRegionList_2G_All = { + {region=0, text="0: Ch1~11"}, + {region=1, text="1: Ch1~13"}, + {region=2, text="2: Ch10~11"}, + {region=3, text="3: Ch10~13"}, + {region=4, text="4: Ch14"}, + {region=5, text="5: Ch1~14"}, + {region=6, text="6: Ch3~9"}, + {region=7, text="7: Ch5~13"}, + {region=31, text="31: Ch1~11, Ch12~14"}, + {region=32, text="32: Ch1~11, Ch12~13"}, + {region=33, text="33: Ch1~14"} +} + +mtkwifi.ChannelList_6G_All = { + {channel= 0 , text="Channel 0 (Auto )", region={}}, + {channel= 1 , text="Channel 1 (5.955 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 5 , text="Channel 5 (5.975 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 9 , text="Channel 9 (5.995 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 13 , text="Channel 13 (6.015 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 17 , text="Channel 17 (6.035 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 21 , text="Channel 21 (6.055 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 25 , text="Channel 25 (6.075 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 29 , text="Channel 29 (6.095 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 33 , text="Channel 33 (6.115 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 37 , text="Channel 37 (6.135 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 41 , text="Channel 41 (6.155 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 45 , text="Channel 45 (6.175 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 49 , text="Channel 49 (6.195 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 53 , text="Channel 53 (6.215 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 57 , text="Channel 57 (6.235 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 61 , text="Channel 61 (6.255 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 65 , text="Channel 65 (6.275 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 69 , text="Channel 69 (6.295 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 73 , text="Channel 73 (6.315 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 77 , text="Channel 77 (6.335 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 81 , text="Channel 81 (6.355 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 85 , text="Channel 85 (6.375 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 89 , text="Channel 89 (6.395 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 93 , text="Channel 93 (6.415 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 97 , text="Channel 97 (6.435 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 101, text="Channel 101 (6.455 GHz)", region={[0]=1, [2]=1, [7]=1}}, + {channel= 105, text="Channel 105 (6.475 GHz)", region={[0]=1, [2]=1, [7]=1}}, + {channel= 109, text="Channel 109 (6.495 GHz)", region={[0]=1, [2]=1, [7]=1}}, + {channel= 113, text="Channel 113 (6.515 GHz)", region={[0]=1, [2]=1}}, + {channel= 117, text="Channel 117 (6.535 GHz)", region={[0]=1, [2]=1}}, + {channel= 121, text="Channel 121 (6.555 GHz)", region={[0]=1, [3]=1}}, + {channel= 125, text="Channel 125 (6.575 GHz)", region={[0]=1, [3]=1}}, + {channel= 129, text="Channel 129 (6.595 GHz)", region={[0]=1, [3]=1}}, + {channel= 133, text="Channel 133 (6.615 GHz)", region={[0]=1, [3]=1}}, + {channel= 137, text="Channel 137 (6.635 GHz)", region={[0]=1, [3]=1}}, + {channel= 141, text="Channel 141 (6.655 GHz)", region={[0]=1, [3]=1}}, + {channel= 145, text="Channel 145 (6.675 GHz)", region={[0]=1, [3]=1}}, + {channel= 149, text="Channel 149 (6.695 GHz)", region={[0]=1, [3]=1}}, + {channel= 153, text="Channel 153 (6.715 GHz)", region={[0]=1, [3]=1}}, + {channel= 157, text="Channel 157 (6.735 GHz)", region={[0]=1, [3]=1}}, + {channel= 161, text="Channel 161 (6.755 GHz)", region={[0]=1, [3]=1}}, + {channel= 165, text="Channel 165 (6.775 GHz)", region={[0]=1, [3]=1}}, + {channel= 169, text="Channel 169 (6.795 GHz)", region={[0]=1, [3]=1}}, + {channel= 173, text="Channel 173 (6.815 GHz)", region={[0]=1, [3]=1}}, + {channel= 177, text="Channel 177 (6.835 GHz)", region={[0]=1, [3]=1}}, + {channel= 181, text="Channel 181 (6.855 GHz)", region={[0]=1, [3]=1}}, + {channel= 185, text="Channel 185 (6.875 GHz)", region={[0]=1, [3]=1}}, + {channel= 189, text="Channel 189 (6.895 GHz)", region={[0]=1, [4]=1}}, + {channel= 193, text="Channel 193 (6.915 GHz)", region={[0]=1, [4]=1}}, + {channel= 197, text="Channel 197 (6.935 GHz)", region={[0]=1, [4]=1}}, + {channel= 201, text="Channel 201 (6.955 GHz)", region={[0]=1, [4]=1}}, + {channel= 205, text="Channel 205 (6.975 GHz)", region={[0]=1, [4]=1}}, + {channel= 209, text="Channel 209 (6.995 GHz)", region={[0]=1, [4]=1}}, + {channel= 213, text="Channel 213 (7.015 GHz)", region={[0]=1, [4]=1}}, + {channel= 217, text="Channel 217 (7.035 GHz)", region={[0]=1, [4]=1}}, + {channel= 221, text="Channel 221 (7.055 GHz)", region={[0]=1, [4]=1}}, + {channel= 225, text="Channel 225 (7.075 GHz)", region={[0]=1, [4]=1}}, + {channel= 229, text="Channel 229 (7.095 GHz)", region={[0]=1, [4]=1}}, + {channel= 233, text="Channel 233 (7.115 GHz)", region={[0]=1, [4]=1}}, +} + +mtkwifi.ChannelList_5G_All = { + {channel=0, text="Channel 0 (Auto )", region={}}, + {channel= 36, text="Channel 36 (5.180 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 40, text="Channel 40 (5.200 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 44, text="Channel 44 (5.220 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 48, text="Channel 48 (5.240 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 52, text="Channel 52 (5.260 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel= 56, text="Channel 56 (5.280 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel= 60, text="Channel 60 (5.300 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel= 64, text="Channel 64 (5.320 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel=100, text="Channel 100 (5.500 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=104, text="Channel 104 (5.520 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=108, text="Channel 108 (5.540 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=112, text="Channel 112 (5.560 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=116, text="Channel 116 (5.580 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=120, text="Channel 120 (5.600 GHz)", region={[1]=1, [7]=1, [11]=1, [12]=1, [13]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=124, text="Channel 124 (5.620 GHz)", region={[1]=1, [7]=1, [12]=1, [13]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=128, text="Channel 128 (5.640 GHz)", region={[1]=1, [7]=1, [12]=1, [13]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=132, text="Channel 132 (5.660 GHz)", region={[1]=1, [7]=1, [9]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=136, text="Channel 136 (5.680 GHz)", region={[1]=1, [7]=1, [9]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=140, text="Channel 140 (5.700 GHz)", region={[1]=1, [7]=1, [9]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=144, text="Channel 144 (5.720 GHz)", region={[12]=1, [13]=1, [14]=1}}, + {channel=149, text="Channel 149 (5.745 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=153, text="Channel 153 (5.765 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=157, text="Channel 157 (5.785 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=161, text="Channel 161 (5.805 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=165, text="Channel 165 (5.825 GHz)", region={[0]=1, [4]=1, [7]=1, [9]=1, [10]=1, [13]=1, [14]=1, [15]=1, [16]=1, [30]=1, [31]=1, [34]=1, [36]=1, [37]=1}}, + {channel=169, text="Channel 169 (5.845 GHz)", region={[15]=1}}, + {channel=173, text="Channel 173 (5.865 GHz)", region={[15]=1, [37]=1}} +} + +mtkwifi.ChannelList_2G_All = { + {channel=0, text="Channel 0 (Auto )", region={}}, + {channel= 1, text="Channel 1 (2412 GHz)", region={[0]=1, [1]=1, [5]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 2, text="Channel 2 (2417 GHz)", region={[0]=1, [1]=1, [5]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 3, text="Channel 3 (2422 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 4, text="Channel 4 (2427 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 5, text="Channel 5 (2432 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 6, text="Channel 6 (2437 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 7, text="Channel 7 (2442 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 8, text="Channel 8 (2447 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 9, text="Channel 9 (2452 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=10, text="Channel 10 (2457 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=11, text="Channel 11 (2462 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=12, text="Channel 12 (2467 GHz)", region={[1]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=13, text="Channel 13 (2472 GHz)", region={[1]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=14, text="Channel 14 (2477 GHz)", region={[4]=1, [5]=1, [31]=1, [33]=1}} +} + +mtkwifi.ChannelList_5G_2nd_80MHZ_ALL = { + {channel=36, text="Ch36(5.180 GHz) - Ch48(5.240 GHz)", chidx=2}, + {channel=52, text="Ch52(5.260 GHz) - Ch64(5.320 GHz)", chidx=6}, + {channel=-1, text="Channel between 64 100", chidx=-1}, + {channel=100, text="Ch100(5.500 GHz) - Ch112(5.560 GHz)", chidx=10}, + {channel=112, text="Ch116(5.580 GHz) - Ch128(5.640 GHz)", chidx=14}, + {channel=-1, text="Channel between 128 132", chidx=-1}, + {channel=132, text="Ch132(5.660 GHz) - Ch144(5.720 GHz)", chidx=18}, + {channel=-1, text="Channel between 144 149", chidx=-1}, + {channel=149, text="Ch149(5.745 GHz) - Ch161(5.805 GHz)", chidx=22} +} + +local AuthModeList = { + "Disable", + "OPEN",--OPENWEP + "Enhanced Open", + "SHARED",--SHAREDWEP + "WEPAUTO", + "WPA2", + "WPA3", + "WPA3-192-bit", + "WPA2PSK", + "WPA3PSK", + "WPAPSKWPA2PSK", + "WPA2PSKWPA3PSK", + "WPA1WPA2", + "IEEE8021X" +} + +local AuthModeList_6G = { + "Enhanced Open", + "WPA3PSK" +} + +local WpsEnableAuthModeList = { + "Disable", + "OPEN",--OPENWEP + "WPA2PSK", + "WPAPSKWPA2PSK" +} + +local WpsEnableAuthModeList_6G = { + "Disable", + "WPA2PSK", + "WPAPSKWPA2PSK" +} + +local ApCliAuthModeList = { + "Disable", + "OPEN", + "SHARED", + "Enhanced Open", + "WPAPSK", + "WPA2PSK", + "WPA3PSK", + -- "WPAPSKWPA2PSK", + -- "WPA2PSKWPA3PSK", + -- "WPA", + -- "WPA2", + -- "WPAWPA2", + -- "8021X", +} + +local EncryptionTypeList = { + "WEP", + "TKIP", + "TKIPAES", + "AES", + "GCMP256" +} + +local EncryptionTypeList_6G = { + "WEP", + "AES", + "GCMP256" +} + +local dbdc_prefix = { + {"ra", "rax"}, + {"rai", "ray"}, + {"rae", "raz"} +} + +local dbdc_apcli_prefix = { + {"apcli", "apclix"}, + {"apclii", "apcliy"}, + {"apclie", "apcliz"} +} + +function mtkwifi.band(mode) + local i = tonumber(mode) + if i == 0 + or i == 1 + or i == 4 + or i == 6 + or i == 7 + or i == 9 + or i == 16 then + return "2.4G" + elseif i == 18 then + return "6G" + else + return "5G" + end +end + + +function mtkwifi.__cfg2list(str) + -- delimeter == ";" + local i = 1 + local list = {} + for k in string.gmatch(str, "([^;]+)") do + list[i] = k + i = i + 1 + end + return list +end + +function mtkwifi.token_set(str, n, v) + -- n start from 1 + -- delimeter == ";" + if not str then return end + local tmp = mtkwifi.__cfg2list(str) + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "invalid value type in token_set, "..type(v)) + return + end + if #tmp < tonumber(n) then + for i=#tmp, tonumber(n) do + if not tmp[i] then + tmp[i] = v -- pad holes with v ! + end + end + else + tmp[n] = v + end + return table.concat(tmp, ";"):gsub("^;*(.-);*$", "%1"):gsub(";+",";") +end + + +function mtkwifi.token_get(str, n, v) + -- n starts from 1 + -- v is the backup in case token n is nil + if not str then return v end + local tmp = mtkwifi.__cfg2list(str) + return tmp[tonumber(n)] or v +end + +function mtkwifi.search_dev_and_profile_orig() + local nixio = require("nixio") + local dir = io.popen("ls /etc/wireless/") + if not dir then return end + local result = {} + -- case 1: mt76xx.dat (best) + -- case 2: mt76xx.n.dat (multiple card of same dev) + -- case 3: mt76xx.n.nG.dat (case 2 plus dbdc and multi-profile, bloody hell....) + for line in dir:lines() do + -- nixio.syslog("debug", "scan "..line) + local tmp = io.popen("find /etc/wireless/"..line.." -type f -name \"*.dat\"") + for datfile in tmp:lines() do + -- nixio.syslog("debug", "test "..datfile) + + repeat do + -- for case 1 + local devname = string.match(datfile, "("..line..").dat") + if devname then + result[devname] = datfile + -- nixio.syslog("debug", "yes "..devname.."="..datfile) + break + end + -- for case 2 + local devname = string.match(datfile, "("..line.."%.%d)%.dat") + if devname then + result[devname] = datfile + -- nixio.syslog("debug", "yes "..devname.."="..datfile) + break + end + -- for case 3 + local devname = string.match(datfile, "("..line.."%.%d%.%dG)%.dat") + if devname then + result[devname] = datfile + -- nixio.syslog("debug", "yes "..devname.."="..datfile) + break + end + end until true + end + end + + for k,v in pairs(result) do + nixio.syslog("debug", "search_dev_and_profile_orig: "..k.."="..v) + end + + return result +end + +function mtkwifi.search_dev_and_profile_l1() + local l1dat = mtkwifi.__get_l1dat() + + if not l1dat then return end + + local nixio = require("nixio") + local result = {} + local dbdc_2nd_if = "" + + for k, dev in ipairs(l1dat) do + dbdc_2nd_if = mtkwifi.token_get(dev.main_ifname, 2, nil) + if dbdc_2nd_if then + result[dev["INDEX"].."."..dev["mainidx"]..".1"] = mtkwifi.token_get(dev.profile_path, 1, nil) + result[dev["INDEX"].."."..dev["mainidx"]..".2"] = mtkwifi.token_get(dev.profile_path, 2, nil) + else + result[dev["INDEX"].."."..dev["mainidx"]] = dev.profile_path + end + end + + for k,v in pairs(result) do + nixio.syslog("debug", "search_dev_and_profile_l1: "..k.."="..v) + end + + return result +end + +function mtkwifi.search_dev_and_profile() + return mtkwifi.search_dev_and_profile_l1() or mtkwifi.search_dev_and_profile_orig() +end + +function mtkwifi.__setup_vifs(cfgs, devname, mainidx, subidx) + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1dat and l1.DEV_RINDEX + + local prefix + local main_ifname + local vifs = {} + local dev_idx = "" + + + prefix = l1dat and l1dat[dridx][devname].ext_ifname or dbdc_prefix[mainidx][subidx] + + dev_idx = string.match(devname, "(%w+)") + + vifs["__prefix"] = prefix + if (cfgs.BssidNum == nil) then + debug_write("BssidNum configuration value not found.") + nixio.syslog("debug","BssidNum configuration value not found.") + return + end + + for j=1,tonumber(cfgs.BssidNum) do + vifs[j] = {} + vifs[j].vifidx = j -- start from 1 + dev_idx = string.match(devname, "(%w+)") + main_ifname = l1dat and l1dat[dridx][devname].main_ifname or dbdc_prefix[mainidx][subidx].."0" + vifs[j].vifname = j == 1 and main_ifname or prefix..(j-1) + if mtkwifi.exists("/sys/class/net/"..vifs[j].vifname) then + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..vifs[j].vifname.."/flags 2>/dev/null")) or 0 + vifs[j].state = flags%2 == 1 and "up" or "down" + end + vifs[j].__ssid = cfgs["SSID"..j] + local rd_pipe_output = mtkwifi.read_pipe("cat /sys/class/net/"..prefix..(j-1).."/address 2>/dev/null") + vifs[j].__bssid = rd_pipe_output and string.match(rd_pipe_output, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") or "?" + + vifs[j].__temp_ssid = mtkwifi.__trim(mtkwifi.read_pipe("iwconfig "..vifs[j].vifname.." | grep ESSID | cut -d : -f 2")) + vifs[j].__temp_channel = mtkwifi.read_pipe("iwconfig "..vifs[j].vifname.." | grep Channel | cut -d = -f 2 | cut -d \" \" -f 1") + if string.gsub(vifs[j].__temp_channel, "^%s*(.-)%s*$", "%1") == "" then + vifs[j].__temp_channel = mtkwifi.read_pipe("iwconfig "..vifs[j].vifname.." | grep Channel | cut -d : -f 3 | cut -d \" \" -f 1") + end + vifs[j].__wirelessmode_table = c_getWMode(vifs[j].vifname) + vifs[j].__temp_wirelessmode = vifs[j].__wirelessmode_table['getwmode'] + + if (vifs[j].__temp_ssid ~= "") then + vifs[j].__ssid = vifs[j].__temp_ssid:gsub("^\"(.-)\"$","%1") + else + vifs[j].__ssid = cfgs["SSID"..j] + end + + if (vifs[j].__temp_channel ~= "" ) then + vifs[j].__channel = vifs[j].__temp_channel + else + vifs[j].__channel = cfgs.Channel + end + + if (vifs[j].__temp_wirelessmode ~= "" and vifs[j].__temp_wirelessmode ~= "0") then + vifs[j].__wirelessmode = vifs[j].__temp_wirelessmode + else + vifs[j].__wirelessmode = mtkwifi.token_get(cfgs.WirelessMode, j, 0) + end + + vifs[j].__authmode = mtkwifi.token_get(cfgs.AuthMode, j, mtkwifi.__split(cfgs.AuthMode,";")[1]) + vifs[j].__encrypttype = mtkwifi.token_get(cfgs.EncrypType, j, mtkwifi.__split(cfgs.EncrypType,";")[1]) + vifs[j].__hidessid = mtkwifi.token_get(cfgs.HideSSID, j, mtkwifi.__split(cfgs.HideSSID,";")[1]) + vifs[j].__noforwarding = mtkwifi.token_get(cfgs.NoForwarding, j, mtkwifi.__split(cfgs.NoForwarding,";")[1]) + vifs[j].__wmmcapable = mtkwifi.token_get(cfgs.WmmCapable, j, mtkwifi.__split(cfgs.WmmCapable,";")[1]) + vifs[j].__txrate = mtkwifi.token_get(cfgs.TxRate, j, mtkwifi.__split(cfgs.TxRate,";")[1]) + vifs[j].__ieee8021x = mtkwifi.token_get(cfgs.IEEE8021X, j, mtkwifi.__split(cfgs.IEEE8021X,";")[1]) + vifs[j].__preauth = mtkwifi.token_get(cfgs.PreAuth, j, mtkwifi.__split(cfgs.PreAuth,";")[1]) + vifs[j].__rekeymethod = mtkwifi.token_get(cfgs.RekeyMethod, j, mtkwifi.__split(cfgs.RekeyMethod,";")[1]) + vifs[j].__rekeyinterval = mtkwifi.token_get(cfgs.RekeyInterval, j, mtkwifi.__split(cfgs.RekeyInterval,";")[1]) + vifs[j].__pmkcacheperiod = mtkwifi.token_get(cfgs.PMKCachePeriod, j, mtkwifi.__split(cfgs.PMKCachePeriod,";")[1]) + vifs[j].__ht_extcha = mtkwifi.token_get(cfgs.HT_EXTCHA, j, mtkwifi.__split(cfgs.HT_EXTCHA,";")[1]) + vifs[j].__radius_server = mtkwifi.token_get(cfgs.RADIUS_Server, j, mtkwifi.__split(cfgs.RADIUS_Server,";")[1]) + vifs[j].__radius_port = mtkwifi.token_get(cfgs.RADIUS_Port, j, mtkwifi.__split(cfgs.RADIUS_Port,";")[1]) + vifs[j].__wepkey_id = mtkwifi.token_get(cfgs.DefaultKeyID, j, mtkwifi.__split(cfgs.DefaultKeyID,";")[1]) + vifs[j].__wscconfmode = mtkwifi.token_get(cfgs.WscConfMode, j, mtkwifi.__split(cfgs.WscConfMode,";")[1]) + vifs[j].__wepkeys = { + cfgs["Key1Str"..j], + cfgs["Key2Str"..j], + cfgs["Key3Str"..j], + cfgs["Key4Str"..j], + } + vifs[j].__wpapsk = cfgs["WPAPSK"..j] + vifs[j].__ht_stbc = mtkwifi.token_get(cfgs.HT_STBC, j, mtkwifi.__split(cfgs.HT_STBC,";")[1]) + vifs[j].__ht_ldpc = mtkwifi.token_get(cfgs.HT_LDPC, j, mtkwifi.__split(cfgs.HT_LDPC,";")[1]) + vifs[j].__vht_stbc = mtkwifi.token_get(cfgs.VHT_STBC, j, mtkwifi.__split(cfgs.VHT_STBC,";")[1]) + vifs[j].__vht_ldpc = mtkwifi.token_get(cfgs.VHT_LDPC, j, mtkwifi.__split(cfgs.VHT_LDPC,";")[1]) + vifs[j].__dls_capable = mtkwifi.token_get(cfgs.DLSCapable, j, mtkwifi.__split(cfgs.DLSCapable,";")[1]) + vifs[j].__apsd_capable = mtkwifi.token_get(cfgs.APSDCapable, j, mtkwifi.__split(cfgs.APSDCapable,";")[1]) + vifs[j].__frag_threshold = mtkwifi.token_get(cfgs.FragThreshold, j, mtkwifi.__split(cfgs.FragThreshold,";")[1]) + vifs[j].__rts_threshold = mtkwifi.token_get(cfgs.RTSThreshold, j, mtkwifi.__split(cfgs.RTSThreshold,";")[1]) + vifs[j].__vht_sgi = mtkwifi.token_get(cfgs.VHT_SGI, j, mtkwifi.__split(cfgs.VHT_SGI,";")[1]) + vifs[j].__vht_bw_signal = mtkwifi.token_get(cfgs.VHT_BW_SIGNAL, j, mtkwifi.__split(cfgs.VHT_BW_SIGNAL,";")[1]) + vifs[j].__ht_protect = mtkwifi.token_get(cfgs.HT_PROTECT, j, mtkwifi.__split(cfgs.HT_PROTECT,";")[1]) + vifs[j].__ht_gi = mtkwifi.token_get(cfgs.HT_GI, j, mtkwifi.__split(cfgs.HT_GI,";")[1]) + vifs[j].__ht_opmode = mtkwifi.token_get(cfgs.HT_OpMode, j, mtkwifi.__split(cfgs.HT_OpMode,";")[1]) + vifs[j].__ht_amsdu = mtkwifi.token_get(cfgs.HT_AMSDU, j, mtkwifi.__split(cfgs.HT_AMSDU,";")[1]) + vifs[j].__ht_autoba = mtkwifi.token_get(cfgs.HT_AutoBA, j, mtkwifi.__split(cfgs.HT_AutoBA,";")[1]) + vifs[j].__igmp_snenable = mtkwifi.token_get(cfgs.IgmpSnEnable, j, mtkwifi.__split(cfgs.IgmpSnEnable,";")[1]) + vifs[j].__wdsenable = mtkwifi.token_get(cfgs.WdsEnable, j, mtkwifi.__split(cfgs.WdsEnable,";")[1]) + + -- VoW + vifs[j].__atc_tp = mtkwifi.token_get(cfgs.VOW_Rate_Ctrl_En, j, mtkwifi.__split(cfgs.VOW_Rate_Ctrl_En,";")[1]) + vifs[j].__atc_min_tp = mtkwifi.token_get(cfgs.VOW_Group_Min_Rate, j, mtkwifi.__split(cfgs.VOW_Group_Min_Rate,";")[1]) + vifs[j].__atc_max_tp = mtkwifi.token_get(cfgs.VOW_Group_Max_Rate, j, mtkwifi.__split(cfgs.VOW_Group_Max_Rate,";")[1]) + vifs[j].__atc_at = mtkwifi.token_get(cfgs.VOW_Airtime_Ctrl_En, j, mtkwifi.__split(cfgs.VOW_Airtime_Ctrl_En,";")[1]) + vifs[j].__atc_min_at = mtkwifi.token_get(cfgs.VOW_Group_Min_Ratio, j, mtkwifi.__split(cfgs.VOW_Group_Min_Ratio,";")[1]) + vifs[j].__atc_max_at = mtkwifi.token_get(cfgs.VOW_Group_Max_Ratio, j, mtkwifi.__split(cfgs.VOW_Group_Max_Ratio,";")[1]) + + -- TODO index by vifname + vifs[vifs[j].vifname] = vifs[j] + + -- OFDMA and MU-MIMO + vifs[j].__muofdma_dlenable = mtkwifi.token_get(cfgs.MuOfdmaDlEnable, j, mtkwifi.__split(cfgs.MuOfdmaDlEnable,";")[1]) + vifs[j].__muofdma_ulenable = mtkwifi.token_get(cfgs.MuOfdmaUlEnable, j, mtkwifi.__split(cfgs.MuOfdmaUlEnable,";")[1]) + vifs[j].__mumimo_dlenable = mtkwifi.token_get(cfgs.MuMimoDlEnable, j, mtkwifi.__split(cfgs.MuMimoDlEnable,";")[1]) + vifs[j].__mumimo_ulenable = mtkwifi.token_get(cfgs.MuMimoUlEnable, j, mtkwifi.__split(cfgs.MuMimoUlEnable,";")[1]) + + end + + return vifs +end + +function mtkwifi.__setup_apcli(cfgs, devname, mainidx, subidx) + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1dat and l1.DEV_RINDEX + + local apcli = {} + local dev_idx = string.match(devname, "(%w+)") + local apcli_prefix = l1dat and l1dat[dridx][devname].apcli_ifname or + dbdc_apcli_prefix[mainidx][subidx] + + local apcli_name = apcli_prefix.."0" + + if mtkwifi.exists("/sys/class/net/"..apcli_name) then + apcli.vifname = apcli_name + apcli.devname = apcli_name + apcli.vifidx = "1" + local rd_pipe_output = mtkwifi.read_pipe("iwconfig "..apcli_name.." | grep ESSID 2>/dev/null") + local ssid = rd_pipe_output and string.match(rd_pipe_output, "ESSID:\"(.*)\"") + if not ssid or ssid == "" then + apcli.status = "Disconnected" + else + apcli.ssid = ssid + apcli.status = "Connected" + end + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..apcli_name.."/flags 2>/dev/null")) or 0 + apcli.state = flags%2 == 1 and "up" or "down" + rd_pipe_output = mtkwifi.read_pipe("cat /sys/class/net/"..apcli_name.."/address 2>/dev/null") + apcli.mac_addr = rd_pipe_output and string.match(rd_pipe_output, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") or "?" + rd_pipe_output = mtkwifi.read_pipe("iwconfig "..apcli_name.." | grep 'Access Point' 2>/dev/null") + apcli.bssid = rd_pipe_output and string.match(rd_pipe_output, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") or "Not-Associated" + return apcli + else + return + end +end + +function mtkwifi.__setup_eths() + local etherInfo = {} + local all_eth_devs = mtkwifi.read_pipe("ls /sys/class/net/ | grep eth | grep -v grep") + if not all_eth_devs or all_eth_devs == "" then + return + end + for ethName in string.gmatch(all_eth_devs, "(eth%d)") do + local ethInfo = {} + ethInfo['ifname'] = ethName + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..ethName.."/flags 2>/dev/null")) or 0 + ethInfo['state'] = flags%2 == 1 and "up" or "down" + ethInfo['mac_addr'] = mtkwifi.read_pipe("cat /sys/class/net/"..ethName.."/address 2>/dev/null") or "?" + table.insert(etherInfo,ethInfo) + end + return etherInfo +end + +function mtkwifi.__is_6890_project() + local str = mtkwifi.read_pipe("cat /etc/vendor_info | grep \"PLATFORM=\"") + str = string.gsub(str, "PLATFORM=", "") + if str:find("6890") then + return true + end + return false +end + +function mtkwifi.get_all_devs() + local nixio = require("nixio") + local devs = {} + local i = 1 -- dev idx + local profiles = mtkwifi.search_dev_and_profile() + local wpa_support = 0 + local wapi_support = 0 + + for devname,profile in mtkwifi.__spairs(profiles, function(a,b) return string.upper(a) < string.upper(b) end) do + local fd = io.open(profile,"r") + if not fd then + nixio.syslog("debug", "cannot find "..profile) + else + fd:close() + local cfgs = mtkwifi.load_profile(profile) + if not cfgs then + debug_write("error loading profile"..profile) + nixio.syslog("err", "error loading "..profile) + return + end + devs[i] = {} + devs[i].vifs = {} + devs[i].apcli = {} + devs[i].devname = devname + devs[i].profile = profile + local tmp = "" + tmp = string.split(devname, ".") + devs[i].maindev = tmp[1] + devs[i].mainidx = tonumber(tmp[2]) or 1 + devs[i].subdev = devname + devs[i].subidx = string.match(tmp[3] or "", "(%d+)")=="2" and 2 or 1 + devs[i].devband = tonumber(tmp[3]) + if devs[i].devband then + devs[i].multiprofile = true + devs[i].dbdc = true + devs[i].dbdcBandName = (profile:match("2[gG]") and "2.4G") or (profile:match("5[gG]") and "5G") + if not devs[i].dbdcBandName then + -- Make 1st band as 2.4G and 2nd band as 5G. + devs[i].dbdcBandName = (devs[i].devband == 1) and "2.4G" or "5G" + end + end + + devs[i].ApCliEnable = cfgs.ApCliEnable + devs[i].WirelessMode = string.split(cfgs.WirelessMode,";")[1] + devs[i].WirelessModeList = {} + for key, value in pairs(DevicePropertyMap) do + local found = string.find(string.upper(devname), string.upper(value.device)) + if found then + for k=1,#value.band do + devs[i].WirelessModeList[tonumber(value.band[k])] = WirelessModeList[tonumber(value.band[k])] + end + + if mtkwifi.__is_6890_project() then + if devs[i].dbdc then + nixio.syslog("debug", "6890 MiFi, change maxVif to 4") + devs[i].maxVif = 4 + else + nixio.syslog("debug", "6890 CPE, change maxVif to 8") + devs[i].maxVif = 8 + end + elseif devs[i].dbdc == true then + devs[i].maxVif = value.maxDBDCVif or value.maxVif/2 + else + devs[i].maxVif = value.maxVif or 16 + end + + devs[i].maxTxStream = value.maxTxStream + devs[i].maxRxStream = value.maxRxStream + devs[i].invalidChBwList = value.invalidChBwList + devs[i].isPowerBoostSupported = value.isPowerBoostSupported + devs[i].wdsBand = value.wdsBand + devs[i].mimoBand = value.mimoBand + devs[i].isMultiAPSupported = value.isMultiAPSupported + devs[i].isWPA3_192bitSupported = value.isWPA3_192bitSupported + end + end + devs[i].WscConfMode = cfgs.WscConfMode + devs[i].AuthModeList = AuthModeList + devs[i].AuthModeList_6G = AuthModeList_6G + devs[i].WpsEnableAuthModeList = WpsEnableAuthModeList + devs[i].WpsEnableAuthModeList_6G = WpsEnableAuthModeList_6G + + if wpa_support == 1 then + table.insert(devs[i].AuthModeList,"WPAPSK") + table.insert(devs[i].AuthModeList,"WPA") + end + + if wapi_support == 1 then + table.insert(devs[i].AuthModeList,"WAIPSK") + table.insert(devs[i].AuthModeList,"WAICERT") + end + devs[i].ApCliAuthModeList = ApCliAuthModeList + devs[i].EncryptionTypeList = EncryptionTypeList + devs[i].EncryptionTypeList_6G = EncryptionTypeList_6G + devs[i].Channel = tonumber(cfgs.Channel) + devs[i].DBDC_MODE = tonumber(cfgs.DBDC_MODE) + devs[i].band = devs[i].devband or mtkwifi.band(string.split(cfgs.WirelessMode,";")[1]) + + if cfgs.MUTxRxEnable then + if tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)==0 + and tonumber(cfgs.ITxBfEn)==0 + then devs[i].__mimo = 0 + elseif tonumber(cfgs.ETxBfEnCond)==0 + and tonumber(cfgs.MUTxRxEnable)==0 + and tonumber(cfgs.ITxBfEn)==1 + then devs[i].__mimo = 1 + elseif tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)==0 + and tonumber(cfgs.ITxBfEn)==1 + then devs[i].__mimo = 2 + elseif tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)>0 + and tonumber(cfgs.ITxBfEn)==0 + then devs[i].__mimo = 3 + elseif tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)>0 + and tonumber(cfgs.ITxBfEn)==1 + then devs[i].__mimo = 4 + else devs[i].__mimo = 5 + end + end + + if cfgs.HT_BW == "0" or not cfgs.HT_BW then + devs[i].__bw = "20" + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "0" or not cfgs.VHT_BW then + if cfgs.HT_BSSCoexistence == "0" or not cfgs.HT_BSSCoexistence then + devs[i].__bw = "40" + else + devs[i].__bw = "60" -- 20/40 coexist + end + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "1" then + devs[i].__bw = "80" + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "2" then + devs[i].__bw = "160" + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "3" then + devs[i].__bw = "161" + end + + devs[i].vifs = mtkwifi.__setup_vifs(cfgs, devname, devs[i].mainidx, devs[i].subidx) + devs[i].apcli = mtkwifi.__setup_apcli(cfgs, devname, devs[i].mainidx, devs[i].subidx) + + if mtkwifi.exists("cat /etc/wireless/"..devs[i].maindev.."/version") then + local version = mtkwifi.read_pipe("cat /etc/wireless/"..devs[i].maindev.."/version 2>/dev/null") + devs[i].version = (type(version) == "string" and version ~= "") and version or "Unknown: Empty version file!" + else + local vif_name = nil + if devs[i].apcli and devs[i].apcli["state"] == "up" then + vif_name = devs[i].apcli["vifname"] + elseif devs[i].vifs then + for _,vif in ipairs(devs[i].vifs) do + if vif["state"] == "up" then + vif_name = vif["vifname"] + break + end + end + end + if not vif_name then + if tonumber(cfgs.BssidNum) >= 1 then + devs[i].version = "Enable an interface to get the driver version." + elseif devs[i].apcli and devs[i].apcli["state"] ~= "up" then + devs[i].version = "Enable ApCli interface i.e. "..devs[i].apcli["vifname"].." to get the driver version." + else + devs[i].version = "Add an interface to get the driver version." + end + else + local version = mtkwifi.read_pipe("iwpriv "..vif_name.." get_driverinfo") + version = version and version:match("Driver version: (.-)\n") or "" + devs[i].version = version ~= "" and version or "Unknown: Incorrect response from version command!" + end + end + + -- Setup reverse indices by devname + devs[devname] = devs[i] + + if devs[i].apcli then + devs[i][devs[i].apcli.devname] = devs[i].apcli + end + + i = i + 1 + end + end + devs['etherInfo'] = mtkwifi.__setup_eths() + return devs +end + +function mtkwifi.exists(path) + local fp = io.open(path, "rb") + if fp then fp:close() end + return fp ~= nil +end + +function mtkwifi.parse_mac(str) + local macs = {} + local pat = "^[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]$" + + local function ismac(str) + if str:match(pat) then return str end + end + + if not str then return macs end + local t = str:split("\n") + for _,v in pairs(t) do + local mac = ismac(mtkwifi.__trim(v)) + if mac then + table.insert(macs, mac) + end + end + + return macs + -- body +end + + +function mtkwifi.scan_ap(vifname) + os.execute("iwpriv "..vifname.." set SiteSurvey=0") + os.execute("sleep 10") -- depends on your env + local op = c_scanResult(vifname, 0) + local scan_result = op["scanresult"] + local next_line_index = 0 + local cur_index + local total_index = 0 + local ap_list = {} + local xx = {} + local tmp + + while (1) do + for i, line in ipairs(mtkwifi.__lines(scan_result)) do + local is_mac_addr_present = string.match(line, "%s+%x%x:%x%x:%x%x:%x%x:%x%x:%x%x%s+") + -- If the line does not contain any MAC address and length is greater than 40 bytes, + -- then, the line is the header of the get_site_survey page. + local total_str = string.find(line, "Total=") + if total_str == 1 then + total_index = tonumber(line:match("%d+")) + end + + if #line>40 and not is_mac_addr_present then + xx.Ch = {string.find(line, "Ch "),3} + xx.SSID = {string.find(line, "SSID "),32} + local fidx = string.find(line, "SSID_Len") + if fidx then + xx.SSID_len = {fidx,2} + end + xx.BSSID = {string.find(line, "BSSID "),17} + xx.Security = {string.find(line, "Security "),22} + xx.Signal = {string.find(line, "Sig%a%al"),4} + xx.Mode = {string.find(line, "W-Mode"),5} + xx.ExtCh = {string.find(line, "ExtCH"),6} + xx.WPS = {string.find(line, "WPS"),3} + xx.NT = {string.find(line, "NT"),2} + fidx = string.find(line, "OWETranIe") + if fidx then + xx.OWETranIe = {fidx,9} + end + end + + if #line>40 and is_mac_addr_present then + tmp = {} + tmp.channel = mtkwifi.__trim(string.sub(line, xx.Ch[1], xx.Ch[1]+xx.Ch[2])) + if xx.SSID_len then + -- Maximum xx.SSID[2] characters are supported in SSID + tmp.ssid_len = tonumber(mtkwifi.__trim(string.sub(line, xx.SSID_len[1], xx.SSID_len[1]+xx.SSID_len[2]))) or xx.SSID[2] + if tmp.ssid_len > xx.SSID[2] or tmp.ssid_len < 0 then + tmp.ssid_len = xx.SSID[2] + tmp.ssid = string.sub(line, xx.SSID[1], xx.SSID[1]+tmp.ssid_len-1) + else + tmp.ssid = string.sub(line, xx.SSID[1], xx.BSSID[1]-1) + if string.find(tmp.ssid, "0x") == nil then + tmp.ssid = string.sub(line, xx.SSID[1], xx.SSID[1]+tmp.ssid_len-1) + end + end + else + tmp.ssid = mtkwifi.__trim(string.sub(line, xx.SSID[1], xx.SSID[1]+xx.SSID[2])) + tmp.ssid_len = tmp.ssid:len() + end + tmp.bssid = string.upper(mtkwifi.__trim(string.sub(line, xx.BSSID[1], xx.BSSID[1]+xx.BSSID[2]))) + tmp.security = mtkwifi.__trim(string.sub(line, xx.Security[1], xx.Security[1]+xx.Security[2])) + tmp.authmode = mtkwifi.__trim(string.split(tmp.security, "/")[1]) + tmp.encrypttype = mtkwifi.__trim(string.split(tmp.security, "/")[2] or "NONE") + tmp.rssi = mtkwifi.__trim(string.sub(line, xx.Signal[1], xx.Signal[1]+xx.Signal[2])) + tmp.extch = mtkwifi.__trim(string.sub(line, xx.ExtCh[1], xx.ExtCh[1]+xx.ExtCh[2])) + tmp.mode = mtkwifi.__trim(string.sub(line, xx.Mode[1], xx.Mode[1]+xx.Mode[2])) + tmp.wps = mtkwifi.__trim(string.sub(line, xx.WPS[1], xx.WPS[1]+xx.WPS[2])) + tmp.nt = mtkwifi.__trim(string.sub(line, xx.NT[1], xx.NT[1]+xx.NT[2])) + if xx.OWETranIe then + tmp.OWETranIe = mtkwifi.__trim(string.sub(line, xx.OWETranIe[1], xx.OWETranIe[1]+xx.OWETranIe[2])) + end + table.insert(ap_list, tmp) + cur_index = tonumber(line:match("^%d+")) + if cur_index == total_index - 1 then + break; + end + next_line_index = cur_index and cur_index + 1 or next_line_index + end + end + if cur_index and cur_index == next_line_index - 1 then + --scan_result = mtkwifi.read_pipe("iwpriv "..vifname.." get_site_survey "..next_line_index) + if next_line_index == total_index - 1 then + scan_result = nil + else + op = c_scanResult(vifname, next_line_index) + scan_result = op["scanresult"] + end + else + scan_result = nil + end + + if not scan_result or not string.match(scan_result, "%s+%x%x:%x%x:%x%x:%x%x:%x%x:%x%x%s+") then + break + end + end + + return ap_list +end + +function mtkwifi.__any_wsc_enabled(wsc_conf_mode) + if (wsc_conf_mode == "") then + return 0; + end + if (wsc_conf_mode == "7") then + return 1; + end + if (wsc_conf_mode == "4") then + return 1; + end + if (wsc_conf_mode == "2") then + return 1; + end + if (wsc_conf_mode == "1") then + return 1; + end + return 0; +end + +function mtkwifi.__restart_if_wps(devname, ifname, cfgs) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local wsc_conf_mode = "" + + wsc_conf_mode=mtkwifi.token_get(cfgs["WscConfMode"], ssid_index, "") + + os.execute("iwpriv "..ifname.." set WscConfMode=0") + debug_write("iwpriv "..ifname.." set WscConfMode=0") + os.execute("route delete 239.255.255.250") + debug_write("route delete 239.255.255.250") + if(mtkwifi.__any_wsc_enabled(wsc_conf_mode)) then + os.execute("iwpriv "..ifname.." set WscConfMode=7") + debug_write("iwpriv "..ifname.." set WscConfMode=7") + os.execute("route add -host 239.255.255.250 dev br0") + debug_write("route add -host 239.255.255.250 dev br0") + end + + -- execute wps_action.lua file to send signal for current interface + os.execute("lua wps_action.lua "..ifname) + debug_write("lua wps_action.lua "..ifname) + return cfgs +end + +function mtkwifi.restart_8021x(devname, devices) + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1dat and l1.DEV_RINDEX + + local devs = devices or mtkwifi.get_all_devs() + local dev = devs[devname] + local main_ifname = l1dat and l1dat[dridx][devname].main_ifname or dbdc_prefix[mainidx][subidx].."0" + local prefix = l1dat and l1dat[dridx][devname].ext_ifname or dbdc_prefix[mainidx][subidx] + + local ps_cmd = "ps | grep -v grep | grep rt2860apd | grep "..main_ifname.." | awk '{print $1}'" + local pid_cmd = "cat /var/run/rt2860apd_"..devs[devname].vifs[1].vifname..".pid" + local apd_pid = mtkwifi.read_pipe(pid_cmd) or mtkwifi.read_pipe(ps_cmd) + if tonumber(apd_pid) then + os.execute("kill "..apd_pid) + end + + local cfgs = mtkwifi.load_profile(devs[devname].profile) + local auth_mode = cfgs['AuthMode'] + local ieee8021x = cfgs['IEEE8021X'] + local pat_auth_mode = {"WPA$", "WPA;", "WPA2$", "WPA2;", "WPA1WPA2$", "WPA1WPA2;"} + local pat_ieee8021x = {"1$", "1;"} + local apd_en = false + + for _, pat in ipairs(pat_auth_mode) do + if string.find(auth_mode, pat) then + apd_en = true + end + end + + for _, pat in ipairs(pat_ieee8021x) do + if string.find(ieee8021x, pat) then + apd_en = true + end + end + + if not apd_en then + return + end + if prefix == "ra" then + mtkwifi.__fork_exec("rt2860apd -i "..main_ifname.." -p "..prefix) + elseif prefix == "rae" then + mtkwifi.__fork_exec("rtwifi3apd -i "..main_ifname.." -p "..prefix) + elseif prefix == "rai" then + mtkwifi.__fork_exec("rtinicapd -i "..main_ifname.." -p "..prefix) + elseif prefix == "rax" or prefix == "ray" or prefix == "raz" then + mtkwifi.__fork_exec("rt2860apd_x -i "..main_ifname.." -p "..prefix) + end +end + +function mtkwifi.dat2uci(datfile, ucifile) + local shuci = require("shuci") + local cfgs = mtkwifi.load_profile(datfile) + + local uci = {} + + uci["wifi-device"]={} + uci["wifi-device"][".name"] = device + uci["wifi-device"]["type"] = device + uci["wifi-device"]["vendor"] = "ralink" + uci["wifi-device"]["iface"] = {} + + local i = 1 -- index of wifi-iface + + uci["iface"] = {} + while i <= tonumber(cfgs.BssidNum) do + uci["iface"][i] = {} + local iface = uci["iface"][i] + iface["ssid"] = cfgs["SSID"..(i)] + iface["mode"] = "ap" + iface["network"] = "lan" + iface["ifname"] = "ra0" + iface[".name"] = device.."."..iface["ifname"] + + i=i+1 + end + + shuci.encode(uci, ucifile) +end + +function mtkwifi.uci2dat(ucifile, devname, datfile) + local shuci = require("shuci") + local uci = shuci.decode(ucifile) + local cfgs = mtkwifi.load_profile(datfile) or {} + + if not ucifile or not devname then return end + + for _,dev in ipairs(uci["wifi-device"][devname]) do + for k,v in pairs(dev) do + if string.byte(k) ~= string.byte(".") + and string.byte(k) ~= string.byte("_") then + cfgs.k = v + end + end + end + if datfile then + save_profile(cfgs, datfile) + end +end + +function mtkwifi.get_referer_url() + local to_url + local script_name = luci.http.getenv('SCRIPT_NAME') + local http_referer = luci.http.getenv('HTTP_REFERER') + if script_name and http_referer then + local fIdx = http_referer:find(script_name,1,true) + if fIdx then + to_url = http_referer:sub(fIdx) + end + end + if not to_url or to_url == "" then + to_url = luci.dispatcher.build_url("admin", "mtk", "wifi") + end + return to_url +end + +function mtkwifi.save_read_easymesh_profile(easymesh_cfgs) + if not easymesh_cfgs then + return + end + local easymesh_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__read_easymesh_profile_path()) + if not mtkwifi.exists(easymesh_applied_path) then + os.execute("cp -f "..mtkwifi.__read_easymesh_profile_path().." "..easymesh_applied_path) + end + + local fd = io.open(mtkwifi.__read_easymesh_profile_path(), "w") + if not fd then return end + table.sort(easymesh_cfgs, function(a,b) return avalue:"..v..",") + end + fd:close() + + mtkwifi.save_easymesh_profile_to_nvram() + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.save_easymesh_profile_to_nvram() + if not pcall(require, "mtknvram") then + return + end + local nvram = require("mtknvram") + local merged_easymesh_dev1_path = "/tmp/mtk/wifi/merged_easymesh_dev1.dat" + local l1dat, l1 = mtkwifi.__get_l1dat() + local dev1_profile_paths + local dev1_profile_path_table = l1 and l1.l1_zone_to_path("dev1") + if not next(dev1_profile_path_table) then + return + end + dev1_profile_paths = table.concat(dev1_profile_path_table, " ") + -- Uncomment below two statements when there is sufficient space in dev1 NVRAM zone to store EasyMesh Agent's BSS Cfgs Settings. + -- mtkwifi.__prepare_easymesh_bss_nvram_cfgs() + -- os.execute("cat "..dev1_profile_paths.." "..mtkwifi.__read_easymesh_profile_path().." "..mtkwifi.__easymesh_bss_cfgs_nvram_path().." > "..merged_easymesh_dev1_path.." 2>/dev/null") + -- Comment or remove below line once above requirement is met. + os.execute("cat "..dev1_profile_paths.." "..mtkwifi.__read_easymesh_profile_path().." > "..merged_easymesh_dev1_path.." 2>/dev/null") + nvram.nvram_save_profile(merged_easymesh_dev1_path, "dev1") + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.save_easymesh_mapd_profile(easymesh_mapd_cfgs) + if not easymesh_mapd_cfgs then + return + end + local fd = io.open(mtkwifi.__easymesh_mapd_profile_path(), "w") + if not fd then return end + table.sort(easymesh_mapd_cfgs, function(a,b) return a/dev/null 2>&1") +end + +function mtkwifi.save_write_easymesh_profile(easymesh_mapd_cfgs) + if not easymesh_mapd_cfgs then + return + end + local fd = io.open(mtkwifi.__write_easymesh_profile_path(), "w") + if not fd then return end + table.sort(easymesh_mapd_cfgs, function(a,b) return a/dev/null 2>&1") +end + +function mtkwifi.__read_easymesh_profile_path() + return "/etc/map/mapd_cfg" +end + +function mtkwifi.__write_easymesh_profile_path() + return "/etc/map/mapd_user.cfg" +end + +function mtkwifi.__easymesh_mapd_profile_path() + return "/etc/mapd_strng.conf" +end + +function mtkwifi.__easymesh_bss_cfgs_path() + return "/etc/map/wts_bss_info_config" +end + +function mtkwifi.__easymesh_bss_cfgs_nvram_path() + local p = "/tmp/mtk/wifi/wts_bss_info_config.nvram" + os.execute("mkdir -p /tmp/mtk/wifi") + return p +end + +function mtkwifi.get_easymesh_al_mac(devRole) + local r = {} + local mapd_app_cfgs = mtkwifi.load_profile("/etc/map/1905d.cfg") + if not mapd_app_cfgs then + r['status'] = "Failed to load /etc/map/1905d.cfg file!" + else + r['status'] = 'SUCCESS' + if tonumber(devRole) == 1 then + r['al_mac'] = mapd_app_cfgs['map_controller_alid'] + else + r['al_mac'] = mapd_app_cfgs['map_agent_alid'] + end + --local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + --if easymesh_cfgs['MapAlMac'] ~= r['al_mac'] then + -- easymesh_cfgs['MapAlMac'] = r['al_mac'] + -- mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + --end + end + return r +end + +function mtkwifi.get_easymesh_on_boarded_iface_info() + local r = {} + r['status'] = "ERROR" + r['staBhInfStr'] = "" + r['profile'] = "" + local devs = mtkwifi.get_all_devs() + for _, dev in ipairs(devs) do + if dev.apcli and dev.apcli.status == "Connected" then + r['status'] = "SUCCESS" + r['staBhInfStr'] = r['staBhInfStr']..dev.apcli.vifname..';' + r['profile'] = r['profile']..dev.profile..';' + end + end + return r +end + +function mtkwifi.load_easymesh_bss_cfgs() + local fd = io.open(mtkwifi.__easymesh_bss_cfgs_path(), "r") + if not fd then + return + end + local content = fd:read("*all") + fd:close() + + local cfgs = {} + cfgs['wildCardAlMacCfgs'] = {} + cfgs['distinctAlMacCfgs'] = {} + local tmp = {} + + -- convert profile into lua table + for _,line in ipairs(mtkwifi.__lines(content)) do + -- Trim only leading space characters + line = line:gsub("^%s*(.-)$","%1") + if string.byte(line) ~= string.byte("#") then + local b,e,lineNo,alMac,band = string.find(line, "^(%d+),(%x%x:%x%x:%x%x:%x%x:%x%x:%x%x)%s+(%d+x)%s+") + if band then + alMac = alMac:upper() + local bssInfoIdx + if tmp[alMac] then + if tmp[alMac][band] then + bssInfoIdx = mtkwifi.get_table_length(tmp[alMac][band]) + 1 + tmp[alMac][band][bssInfoIdx] = {} + else + bssInfoIdx = 1 + tmp[alMac][band] = {} + tmp[alMac][band][bssInfoIdx] = {} + end + else + bssInfoIdx = 1 + tmp[alMac] = {} + tmp[alMac][band] = {} + tmp[alMac][band][bssInfoIdx] = {} + end + local tokIdx, token = 0, nil + local bssLineStr = line:sub(e+1) + local ssid = string.gsub(bssLineStr, "(%s0x%d+).*", "") + tmp[alMac][band][bssInfoIdx]['ssid'] = ssid + local security = string.match(bssLineStr, "0x%S+ 0x%S+") + tmp[alMac][band][bssInfoIdx]['authMode'] = security:sub(1,6) + tmp[alMac][band][bssInfoIdx]['encType'] = security:sub(8,13) + local newBssLineStr = string.match(bssLineStr, "0x%S+ %S.*") + local updateBssLineStr = string.gsub(newBssLineStr, "0x%S+ 0x%S+%s", "") + local passPhrase = string.gsub(updateBssLineStr, "%s%d %d %S+ %d+ %S+ %S+", "") + tmp[alMac][band][bssInfoIdx]['passPhrase'] = passPhrase + local restBssLineStr = string.match(updateBssLineStr, "%d %d %S+ %d+ %S+ %S+") + for token in string.gmatch(restBssLineStr, "(%S+)%s?") do + tokIdx = tokIdx + 1 + if tokIdx == 1 then + tmp[alMac][band][bssInfoIdx]['isBhBssSupported'] = token + elseif tokIdx == 2 then + tmp[alMac][band][bssInfoIdx]['isFhBssSupported'] = token + elseif tokIdx == 3 then + tmp[alMac][band][bssInfoIdx]['isHidden'] = token + elseif tokIdx == 4 then + tmp[alMac][band][bssInfoIdx]['fhVlanId'] = token + elseif tokIdx == 5 then + tmp[alMac][band][bssInfoIdx]['primVlan'] = token + elseif tokIdx == 6 then + tmp[alMac][band][bssInfoIdx]['defPCP'] = token + else + nixio.syslog("warning", "load_easymesh_bss_cfgs: Extra Unknown Parameters "..line) + end + end + if tokIdx == 6 then + if alMac == "FF:FF:FF:FF:FF:FF" then + cfgs['wildCardAlMacCfgs']['FF:FF:FF:FF:FF:FF'] = tmp[alMac] + else + cfgs['distinctAlMacCfgs'][alMac] = tmp[alMac] + end + else + tmp[alMac][band][bssInfoIdx] = nil + nixio.syslog("warning", "load_easymesh_bss_cfgs: skip invalid line "..line) + end + else + nixio.syslog("warning", "load_easymesh_bss_cfgs: skip line without 'LineNumber,AL-MAC Band' "..line) + end + else + nixio.syslog("warning", "load_easymesh_bss_cfgs: skip comment line "..line) + end + end + return cfgs +end + +function mtkwifi.save_easymesh_bss_cfgs(cfgs) + if not cfgs or not cfgs['wildCardAlMacCfgs'] or not cfgs['distinctAlMacCfgs'] then + return + end + local easymesh_bss_cfg_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__easymesh_bss_cfgs_path()) + if not mtkwifi.exists(easymesh_bss_cfg_applied_path) then + os.execute("cp -f "..mtkwifi.__easymesh_bss_cfgs_path().." "..easymesh_bss_cfg_applied_path) + end + + local fd = io.open(mtkwifi.__easymesh_bss_cfgs_path(), "w") + if not fd then + return + end + + local lineIdx = 0 + -- First write distinct AL-MAC cfgs; then write wildcard AL-MAC(FF:FF:FF:FF:FF:FF) cfgs + for alMac,alMacTbl in pairs(cfgs['distinctAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write(lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + for alMac,alMacTbl in pairs(cfgs['wildCardAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write(lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + fd:close() + os.execute("sync "..mtkwifi.__easymesh_bss_cfgs_path().." >/dev/null 2>&1") + + -- Uncomment below line when there is sufficient space in dev1 NVRAM zone to store EasyMesh Agent's BSS Cfgs Settings. + -- mtkwifi.save_easymesh_profile_to_nvram() +end + +function mtkwifi.__prepare_easymesh_bss_nvram_cfgs() + local fd = io.open(mtkwifi.__easymesh_bss_cfgs_nvram_path(), "w") + if not fd then + return + end + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + local lineIdx = 0 + -- First write distinct AL-MAC cfgs; then write wildcard AL-MAC(FF:FF:FF:FF:FF:FF) cfgs + for alMac,alMacTbl in pairs(cfgs['distinctAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write('EasyMeshBssCfgsLine'..lineIdx..'='..lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + for alMac,alMacTbl in pairs(cfgs['wildCardAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write('EasyMeshBssCfgsLine'..lineIdx..'='..lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + fd:write('EasyMeshTotalBssCfgsLines='..lineIdx..'\n') + fd:close() + os.execute("sync >/dev/null 2>&1") +end + +return mtkwifi diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.luac new file mode 100644 index 000000000000..e7211857a1b7 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.lua new file mode 100644 index 000000000000..8883835f3037 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.lua @@ -0,0 +1,175 @@ +--[[ +nixio - Linux I/O library for lua + +Copyright 2009 Steven Barth + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local table = require "table" +local nixio = require "nixio" +local type, ipairs, setmetatable = type, ipairs, setmetatable +require "nixio.util" + + +module ("nixio.fs", function(m) setmetatable(m, {__index = nixio.fs}) end) + + +function readfile(path, limit) + local fd, code, msg = nixio.open(path, "r") + local data + if not fd then + return nil, code, msg + end + + data, code, msg = fd:readall(limit) + + fd:close() + return data, code, msg +end + + +function writefile(path, data) + local fd, code, msg, stat = nixio.open(path, "w") + if not fd then + return nil, code, msg + end + + stat, code, msg = fd:writeall(data) + + fd:close() + return stat, code, msg +end + +function datacopy(src, dest, size) + local fdin, code, msg = nixio.open(src, "r") + if not fdin then + return nil, code, msg + end + + local fdout, code, msg = nixio.open(dest, "w") + if not fdout then + return nil, code, msg + end + + local stat, code, msg, sent = fdin:copy(fdout, size) + fdin:close() + fdout:close() + + return stat, code, msg, sent +end + +function copy(src, dest) + local stat, code, msg, res = nixio.fs.lstat(src) + if not stat then + return nil, code, msg + end + + if stat.type == "dir" then + if nixio.fs.stat(dest, type) ~= "dir" then + res, code, msg = nixio.fs.mkdir(dest) + else + stat = true + end + elseif stat.type == "lnk" then + res, code, msg = nixio.fs.symlink(nixio.fs.readlink(src), dest) + elseif stat.type == "reg" then + res, code, msg = datacopy(src, dest) + end + + if not res then + return nil, code, msg + end + + nixio.fs.utimes(dest, stat.atime, stat.mtime) + + if nixio.fs.lchown then + nixio.fs.lchown(dest, stat.uid, stat.gid) + end + + if stat.type ~= "lnk" then + nixio.fs.chmod(dest, stat.modedec) + end + + return true +end + +function move(src, dest) + local stat, code, msg = nixio.fs.rename(src, dest) + if not stat and code == nixio.const.EXDEV then + stat, code, msg = copy(src, dest) + if stat then + stat, code, msg = nixio.fs.unlink(src) + end + end + return stat, code, msg +end + +function mkdirr(dest, mode) + if nixio.fs.stat(dest, "type") == "dir" then + return true + else + local stat, code, msg = nixio.fs.mkdir(dest, mode) + if not stat and code == nixio.const.ENOENT then + stat, code, msg = mkdirr(nixio.fs.dirname(dest), mode) + if stat then + stat, code, msg = nixio.fs.mkdir(dest, mode) + end + end + return stat, code, msg + end +end + +local function _recurse(cb, src, dest) + local type = nixio.fs.lstat(src, "type") + if type ~= "dir" then + return cb(src, dest) + else + local stat, se, code, msg, s, c, m = true, nixio.const.sep + if dest then + s, c, m = cb(src, dest) + stat, code, msg = stat and s, c or code, m or msg + end + + for e in nixio.fs.dir(src) do + if dest then + s, c, m = _recurse(cb, src .. se .. e, dest .. se .. e) + else + s, c, m = _recurse(cb, src .. se .. e) + end + stat, code, msg = stat and s, c or code, m or msg + end + + if not dest then -- Postfix + s, c, m = cb(src) + stat, code, msg = stat and s, c or code, m or msg + end + + return stat, code, msg + end +end + +function copyr(src, dest) + return _recurse(copy, src, dest) +end + +function mover(src, dest) + local stat, code, msg = nixio.fs.rename(src, dest) + if not stat and code == nixio.const.EXDEV then + stat, code, msg = _recurse(copy, src, dest) + if stat then + stat, code, msg = _recurse(nixio.fs.remove, src) + end + end + return stat, code, msg +end + +function remover(src) + return _recurse(nixio.fs.remove, src) +end \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.luac new file mode 100644 index 000000000000..e64a3381f977 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.lua new file mode 100644 index 000000000000..63d2f6214796 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.lua @@ -0,0 +1,270 @@ +--[[ +nixio - Linux I/O library for lua + +Copyright 2009 Steven Barth + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local table = require "table" +local nixio = require "nixio" +local getmetatable, assert, pairs, type = getmetatable, assert, pairs, type +local tostring = tostring + +module "nixio.util" + +local BUFFERSIZE = nixio.const.buffersize +local ZIOBLKSIZE = 65536 +local socket = nixio.meta_socket +local tls_socket = nixio.meta_tls_socket +local file = nixio.meta_file +local uname = nixio.uname() +local ZBUG = uname.sysname == "Linux" and uname.release:sub(1, 3) == "2.4" + +function consume(iter, append) + local tbl = append or {} + if iter then + for obj in iter do + tbl[#tbl+1] = obj + end + end + return tbl +end + +local meta = {} + +function meta.is_socket(self) + return (getmetatable(self) == socket) +end + +function meta.is_tls_socket(self) + return (getmetatable(self) == tls_socket) +end + +function meta.is_file(self) + return (getmetatable(self) == file) +end + +function meta.readall(self, len) + local block, code, msg = self:read(len or BUFFERSIZE) + + if not block then + return nil, code, msg, "" + elseif #block == 0 then + return "", nil, nil, "" + end + + local data, total = {block}, #block + + while not len or len > total do + block, code, msg = self:read(len and (len - total) or BUFFERSIZE) + + if not block then + return nil, code, msg, table.concat(data) + elseif #block == 0 then + break + end + + data[#data+1], total = block, total + #block + end + + local data = #data > 1 and table.concat(data) or data[1] + return data, nil, nil, data +end +meta.recvall = meta.readall + +function meta.writeall(self, data) + data = tostring(data) + local sent, code, msg = self:write(data) + + if not sent then + return nil, code, msg, 0 + end + + local total = sent + + while total < #data do + sent, code, msg = self:write(data, total) + + if not sent then + return nil, code, msg, total + end + + total = total + sent + end + + return total, nil, nil, total +end +meta.sendall = meta.writeall + +function meta.linesource(self, limit) + limit = limit or BUFFERSIZE + local buffer = "" + local bpos = 0 + return function(flush) + local line, endp, _ + + if flush then + line = buffer:sub(bpos + 1) + buffer = type(flush) == "string" and flush or "" + bpos = 0 + return line + end + + while not line do + _, endp, line = buffer:find("(.-)\r?\n", bpos + 1) + if line then + bpos = endp + return line + elseif #buffer < limit + bpos then + local newblock, code, msg = self:read(limit + bpos - #buffer) + if not newblock then + return nil, code, msg + elseif #newblock == 0 then + return nil + end + buffer = buffer:sub(bpos + 1) .. newblock + bpos = 0 + else + return nil, 0 + end + end + end +end + +function meta.blocksource(self, bs, limit) + bs = bs or BUFFERSIZE + return function() + local toread = bs + if limit then + if limit < 1 then + return nil + elseif limit < toread then + toread = limit + end + end + + local block, code, msg = self:read(toread) + + if not block then + return nil, code, msg + elseif #block == 0 then + return nil + else + if limit then + limit = limit - #block + end + + return block + end + end +end + +function meta.sink(self, close) + return function(chunk, src_err) + if not chunk and not src_err and close then + if self.shutdown then + self:shutdown() + end + self:close() + elseif chunk and #chunk > 0 then + return self:writeall(chunk) + end + return true + end +end + +function meta.copy(self, fdout, size) + local source = self:blocksource(nil, size) + local sink = fdout:sink() + local sent, chunk, code, msg = 0 + + repeat + chunk, code, msg = source() + sink(chunk, code, msg) + sent = chunk and (sent + #chunk) or sent + until not chunk + return not code and sent or nil, code, msg, sent +end + +function meta.copyz(self, fd, size) + local sent, lsent, code, msg = 0 + local splicable + + if not ZBUG and self:is_file() then + local ftype = self:stat("type") + if nixio.sendfile and fd:is_socket() and ftype == "reg" then + repeat + lsent, code, msg = nixio.sendfile(fd, self, size or ZIOBLKSIZE) + if lsent then + sent = sent + lsent + size = size and (size - lsent) + end + until (not lsent or lsent == 0 or (size and size == 0)) + if lsent or (not lsent and sent == 0 and + code ~= nixio.const.ENOSYS and code ~= nixio.const.EINVAL) then + return lsent and sent, code, msg, sent + end + elseif nixio.splice and not fd:is_tls_socket() and ftype == "fifo" then + splicable = true + end + end + + if nixio.splice and fd:is_file() and not splicable then + splicable = not self:is_tls_socket() and fd:stat("type") == "fifo" + end + + if splicable then + repeat + lsent, code, msg = nixio.splice(self, fd, size or ZIOBLKSIZE) + if lsent then + sent = sent + lsent + size = size and (size - lsent) + end + until (not lsent or lsent == 0 or (size and size == 0)) + if lsent or (not lsent and sent == 0 and + code ~= nixio.const.ENOSYS and code ~= nixio.const.EINVAL) then + return lsent and sent, code, msg, sent + end + end + + return self:copy(fd, size) +end + +if tls_socket then + function tls_socket.close(self) + return self.socket:close() + end + + function tls_socket.getsockname(self) + return self.socket:getsockname() + end + + function tls_socket.getpeername(self) + return self.socket:getpeername() + end + + function tls_socket.getsockopt(self, ...) + return self.socket:getsockopt(...) + end + tls_socket.getopt = tls_socket.getsockopt + + function tls_socket.setsockopt(self, ...) + return self.socket:setsockopt(...) + end + tls_socket.setopt = tls_socket.setsockopt +end + +for k, v in pairs(meta) do + file[k] = v + socket[k] = v + if tls_socket then + tls_socket[k] = v + end +end diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.luac new file mode 100644 index 000000000000..5c91bef59acf Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.lua new file mode 100755 index 000000000000..c90e06f82a16 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.lua @@ -0,0 +1,128 @@ +#!/usr/bin/env lua + +--[[ + * A pure lua library to translate between lua table and uci config + * + * For UCI: http://wiki.openwrt.org/doc/techref/uci + * http://wiki.openwrt.org/doc/uci + * + * Copyright (C) 2015 Hua Shao + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] + + +local shuci = {} + +function shuci.decode(path) + function file_exists(name) + if not name then return false end + local f = io.open(name,"r") + if f then io.close(f) return true else return false end + end + local function linebreaker(str) + local i,_ = string.find(str, "([^%s])") + if not i then return nil end + if string.find(str, "config%s+%w+") then + local i,j,k,v = string.find(str, "config%s+([%w-_]+)%s*['\"]*([^%s\'\"]*)") + return "section", k, v + elseif string.find(str, "option%s+%w+") then + local i,j,k,v = string.find(str, "option%s+([%w-_]+)%s*['\"]([^'\"]+)['\"]") + if not k or not v then + i,j,k,v = string.find(str, "option%s+([%w-_]+)%s*['\"]*([^%s\'\"]*)") + end + return "option", k, v + elseif string.find(str, "list%s+%w+") then + local i,j,k,v = string.find(str, "list%s+([%w-_]+)%s*['\"]([^'\"]+)['\"]") + if not k or not v then + i,j,k,v = string.find(str, "list%s+([%w-_]+)%s*['\"]*([^%s\'\"]*)") + end + return "list", k, v + else + print("invalid line!", str) + return nil + end + end + + if not file_exists(path) then + return + end + + local _sect_ = nil + local t = {} + for line in io.lines(path) do + local _type, _name, _value = linebreaker(line) + if _type == "section" then + if not t[_name] then t[_name] = {} end + -- be careful of anonymous sections + if not _value or _value == "" then _value = #t[_name]+1 end + t[_name][_value] = {} + _sect_ = t[_name][_value] + end + if _type == "option" then + if _name and _value then + _sect_[_name] = _value + end + end + if _type == "list" and _name and _value then + local idx + if not _sect_[_name] then + _sect_[_name] = {} + _sect_[_name][1] = _value + else + idx = #_sect_[_name] + _sect_[_name][idx+1] = _value + end + end + end + + return t +end + + +function shuci.encode(t, path) + local dump = io.write + if path then + local fp = io.open(path, "a+") + dump = function(str) fp:write(str) end + end + for _sect_type,_ in pairs(t) do + for _name,_sect in pairs(t[_sect_type]) do + dump(string.format("config\t%s\t'%s'\n", _sect_type, _name)) + for k,v in pairs(_sect) do + if type(v) == "table" then + for _,vv in ipairs(v) do + dump(string.format("\tlist\t%s\t'%s'\n",k,vv)) + end + elseif type(v) == "string" and k ~= ".name" then + dump(string.format("\toption\t%s\t'%s'\n",k,v)) + elseif type(v) == "number" and k ~= ".name" then + dump(string.format("\toption\t%s\t'%s'\n",k,tonumber(v))) + end + end + dump("\n") + end + end +end + + +function shuci.dump(t, indent) + if not indent then indent = 0 end + for k,v in pairs(t) do + if type(v) == "table" then + print(string.rep(" ",indent)..k..":") + shuci.dump(v, indent+4) + else + print(string.rep(" ",indent)..k..":"..v) + end + end +end + +return shuci diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.luac new file mode 100644 index 000000000000..fdef5f4c8d94 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.lua new file mode 100644 index 000000000000..d1c0b1649245 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.lua @@ -0,0 +1,149 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") + +local _M = socket + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function _M.connect4(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet") +end + +function _M.connect6(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet6") +end + +function _M.bind(host, port, backlog) + if host == "*" then host = "0.0.0.0" end + local addrinfo, err = socket.dns.getaddrinfo(host); + if not addrinfo then return nil, err end + local sock, res + err = "no info on address" + for i, alt in base.ipairs(addrinfo) do + if alt.family == "inet" then + sock, err = socket.tcp4() + else + sock, err = socket.tcp6() + end + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + res, err = sock:bind(alt.addr, port) + if not res then + sock:close() + else + res, err = sock:listen(backlog) + if not res then + sock:close() + else + return sock + end + end + end + return nil, err +end + +_M.try = _M.newtry() + +function _M.choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +local sourcet, sinkt = {}, {} +_M.sourcet = sourcet +_M.sinkt = sinkt + +_M.BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +_M.sink = _M.choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +_M.source = _M.choose(sourcet) + +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.luac new file mode 100644 index 000000000000..cd179dcd5590 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.lua new file mode 100644 index 000000000000..bd528caa2ed1 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.lua @@ -0,0 +1,329 @@ +----------------------------------------------------------------------------- +-- FTP support for the Lua language +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local table = require("table") +local string = require("string") +local math = require("math") +local socket = require("socket") +local url = require("socket.url") +local tp = require("socket.tp") +local ltn12 = require("ltn12") +socket.ftp = {} +local _M = socket.ftp +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout in seconds before the program gives up on a connection +_M.TIMEOUT = 60 +-- default port for ftp service +local PORT = 21 +-- this is the default anonymous password. used when no password is +-- provided in url. should be changed to your e-mail. +_M.USER = "ftp" +_M.PASSWORD = "anonymous@anonymous.org" + +----------------------------------------------------------------------------- +-- Low level FTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function _M.open(server, port, create) + local tp = socket.try(tp.connect(server, port or PORT, _M.TIMEOUT, create)) + local f = base.setmetatable({ tp = tp }, metat) + -- make sure everything gets closed in an exception + f.try = socket.newtry(function() f:close() end) + return f +end + +function metat.__index:portconnect() + self.try(self.server:settimeout(_M.TIMEOUT)) + self.data = self.try(self.server:accept()) + self.try(self.data:settimeout(_M.TIMEOUT)) +end + +function metat.__index:pasvconnect() + self.data = self.try(socket.tcp()) + self.try(self.data:settimeout(_M.TIMEOUT)) + self.try(self.data:connect(self.pasvt.address, self.pasvt.port)) +end + +function metat.__index:login(user, password) + self.try(self.tp:command("user", user or _M.USER)) + local code, reply = self.try(self.tp:check{"2..", 331}) + if code == 331 then + self.try(self.tp:command("pass", password or _M.PASSWORD)) + self.try(self.tp:check("2..")) + end + return 1 +end + +function metat.__index:pasv() + self.try(self.tp:command("pasv")) + local code, reply = self.try(self.tp:check("2..")) + local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" + local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) + self.try(a and b and c and d and p1 and p2, reply) + self.pasvt = { + address = string.format("%d.%d.%d.%d", a, b, c, d), + port = p1*256 + p2 + } + if self.server then + self.server:close() + self.server = nil + end + return self.pasvt.address, self.pasvt.port +end + +function metat.__index:epsv() + self.try(self.tp:command("epsv")) + local code, reply = self.try(self.tp:check("229")) + local pattern = "%((.)(.-)%1(.-)%1(.-)%1%)" + local d, prt, address, port = string.match(reply, pattern) + self.try(port, "invalid epsv response") + self.pasvt = { + address = self.tp:getpeername(), + port = port + } + if self.server then + self.server:close() + self.server = nil + end + return self.pasvt.address, self.pasvt.port +end + + +function metat.__index:port(address, port) + self.pasvt = nil + if not address then + address, port = self.try(self.tp:getsockname()) + self.server = self.try(socket.bind(address, 0)) + address, port = self.try(self.server:getsockname()) + self.try(self.server:settimeout(_M.TIMEOUT)) + end + local pl = math.mod(port, 256) + local ph = (port - pl)/256 + local arg = string.gsub(string.format("%s,%d,%d", address, ph, pl), "%.", ",") + self.try(self.tp:command("port", arg)) + self.try(self.tp:check("2..")) + return 1 +end + +function metat.__index:eprt(family, address, port) + self.pasvt = nil + if not address then + address, port = self.try(self.tp:getsockname()) + self.server = self.try(socket.bind(address, 0)) + address, port = self.try(self.server:getsockname()) + self.try(self.server:settimeout(_M.TIMEOUT)) + end + local arg = string.format("|%s|%s|%d|", family, address, port) + self.try(self.tp:command("eprt", arg)) + self.try(self.tp:check("2..")) + return 1 +end + + +function metat.__index:send(sendt) + self.try(self.pasvt or self.server, "need port or pasv first") + -- if there is a pasvt table, we already sent a PASV command + -- we just get the data connection into self.data + if self.pasvt then self:pasvconnect() end + -- get the transfer argument and command + local argument = sendt.argument or + url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) + if argument == "" then argument = nil end + local command = sendt.command or "stor" + -- send the transfer command and check the reply + self.try(self.tp:command(command, argument)) + local code, reply = self.try(self.tp:check{"2..", "1.."}) + -- if there is not a pasvt table, then there is a server + -- and we already sent a PORT command + if not self.pasvt then self:portconnect() end + -- get the sink, source and step for the transfer + local step = sendt.step or ltn12.pump.step + local readt = { self.tp } + local checkstep = function(src, snk) + -- check status in control connection while downloading + local readyt = socket.select(readt, nil, 0) + if readyt[tp] then code = self.try(self.tp:check("2..")) end + return step(src, snk) + end + local sink = socket.sink("close-when-done", self.data) + -- transfer all data and check error + self.try(ltn12.pump.all(sendt.source, sink, checkstep)) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + -- done with data connection + self.data:close() + -- find out how many bytes were sent + local sent = socket.skip(1, self.data:getstats()) + self.data = nil + return sent +end + +function metat.__index:receive(recvt) + self.try(self.pasvt or self.server, "need port or pasv first") + if self.pasvt then self:pasvconnect() end + local argument = recvt.argument or + url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) + if argument == "" then argument = nil end + local command = recvt.command or "retr" + self.try(self.tp:command(command, argument)) + local code,reply = self.try(self.tp:check{"1..", "2.."}) + if (code >= 200) and (code <= 299) then + recvt.sink(reply) + return 1 + end + if not self.pasvt then self:portconnect() end + local source = socket.source("until-closed", self.data) + local step = recvt.step or ltn12.pump.step + self.try(ltn12.pump.all(source, recvt.sink, step)) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + self.data:close() + self.data = nil + return 1 +end + +function metat.__index:cwd(dir) + self.try(self.tp:command("cwd", dir)) + self.try(self.tp:check(250)) + return 1 +end + +function metat.__index:type(type) + self.try(self.tp:command("type", type)) + self.try(self.tp:check(200)) + return 1 +end + +function metat.__index:greet() + local code = self.try(self.tp:check{"1..", "2.."}) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + return 1 +end + +function metat.__index:quit() + self.try(self.tp:command("quit")) + self.try(self.tp:check("2..")) + return 1 +end + +function metat.__index:close() + if self.data then self.data:close() end + if self.server then self.server:close() end + return self.tp:close() +end + +----------------------------------------------------------------------------- +-- High level FTP API +----------------------------------------------------------------------------- +local function override(t) + if t.url then + local u = url.parse(t.url) + for i,v in base.pairs(t) do + u[i] = v + end + return u + else return t end +end + +local function tput(putt) + putt = override(putt) + socket.try(putt.host, "missing hostname") + local f = _M.open(putt.host, putt.port, putt.create) + f:greet() + f:login(putt.user, putt.password) + if putt.type then f:type(putt.type) end + f:epsv() + local sent = f:send(putt) + f:quit() + f:close() + return sent +end + +local default = { + path = "/", + scheme = "ftp" +} + +local function genericform(u) + local t = socket.try(url.parse(u, default)) + socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") + socket.try(t.host, "missing hostname") + local pat = "^type=(.)$" + if t.params then + t.type = socket.skip(2, string.find(t.params, pat)) + socket.try(t.type == "a" or t.type == "i", + "invalid type '" .. t.type .. "'") + end + return t +end + +_M.genericform = genericform + +local function sput(u, body) + local putt = genericform(u) + putt.source = ltn12.source.string(body) + return tput(putt) +end + +_M.put = socket.protect(function(putt, body) + if base.type(putt) == "string" then return sput(putt, body) + else return tput(putt) end +end) + +local function tget(gett) + gett = override(gett) + socket.try(gett.host, "missing hostname") + local f = _M.open(gett.host, gett.port, gett.create) + f:greet() + f:login(gett.user, gett.password) + if gett.type then f:type(gett.type) end + f:epsv() + f:receive(gett) + f:quit() + return f:close() +end + +local function sget(u) + local gett = genericform(u) + local t = {} + gett.sink = ltn12.sink.table(t) + tget(gett) + return table.concat(t) +end + +_M.command = socket.protect(function(cmdt) + cmdt = override(cmdt) + socket.try(cmdt.host, "missing hostname") + socket.try(cmdt.command, "missing command") + local f = _M.open(cmdt.host, cmdt.port, cmdt.create) + f:greet() + f:login(cmdt.user, cmdt.password) + if type(cmdt.command) == "table" then + local argument = cmdt.argument or {} + local check = cmdt.check or {} + for i,cmd in ipairs(cmdt.command) do + f.try(f.tp:command(cmd, argument[i])) + if check[i] then f.try(f.tp:check(check[i])) end + end + else + f.try(f.tp:command(cmdt.command, cmdt.argument)) + if cmdt.check then f.try(f.tp:check(cmdt.check)) end + end + f:quit() + return f:close() +end) + +_M.get = socket.protect(function(gett) + if base.type(gett) == "string" then return sget(gett) + else return tget(gett) end +end) + +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.luac new file mode 100644 index 000000000000..bbafd3fddc73 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.lua new file mode 100644 index 000000000000..1eb8223b9ddf --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.lua @@ -0,0 +1,104 @@ +----------------------------------------------------------------------------- +-- Canonic header field capitalization +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- +local socket = require("socket") +socket.headers = {} +local _M = socket.headers + +_M.canonic = { + ["accept"] = "Accept", + ["accept-charset"] = "Accept-Charset", + ["accept-encoding"] = "Accept-Encoding", + ["accept-language"] = "Accept-Language", + ["accept-ranges"] = "Accept-Ranges", + ["action"] = "Action", + ["alternate-recipient"] = "Alternate-Recipient", + ["age"] = "Age", + ["allow"] = "Allow", + ["arrival-date"] = "Arrival-Date", + ["authorization"] = "Authorization", + ["bcc"] = "Bcc", + ["cache-control"] = "Cache-Control", + ["cc"] = "Cc", + ["comments"] = "Comments", + ["connection"] = "Connection", + ["content-description"] = "Content-Description", + ["content-disposition"] = "Content-Disposition", + ["content-encoding"] = "Content-Encoding", + ["content-id"] = "Content-ID", + ["content-language"] = "Content-Language", + ["content-length"] = "Content-Length", + ["content-location"] = "Content-Location", + ["content-md5"] = "Content-MD5", + ["content-range"] = "Content-Range", + ["content-transfer-encoding"] = "Content-Transfer-Encoding", + ["content-type"] = "Content-Type", + ["cookie"] = "Cookie", + ["date"] = "Date", + ["diagnostic-code"] = "Diagnostic-Code", + ["dsn-gateway"] = "DSN-Gateway", + ["etag"] = "ETag", + ["expect"] = "Expect", + ["expires"] = "Expires", + ["final-log-id"] = "Final-Log-ID", + ["final-recipient"] = "Final-Recipient", + ["from"] = "From", + ["host"] = "Host", + ["if-match"] = "If-Match", + ["if-modified-since"] = "If-Modified-Since", + ["if-none-match"] = "If-None-Match", + ["if-range"] = "If-Range", + ["if-unmodified-since"] = "If-Unmodified-Since", + ["in-reply-to"] = "In-Reply-To", + ["keywords"] = "Keywords", + ["last-attempt-date"] = "Last-Attempt-Date", + ["last-modified"] = "Last-Modified", + ["location"] = "Location", + ["max-forwards"] = "Max-Forwards", + ["message-id"] = "Message-ID", + ["mime-version"] = "MIME-Version", + ["original-envelope-id"] = "Original-Envelope-ID", + ["original-recipient"] = "Original-Recipient", + ["pragma"] = "Pragma", + ["proxy-authenticate"] = "Proxy-Authenticate", + ["proxy-authorization"] = "Proxy-Authorization", + ["range"] = "Range", + ["received"] = "Received", + ["received-from-mta"] = "Received-From-MTA", + ["references"] = "References", + ["referer"] = "Referer", + ["remote-mta"] = "Remote-MTA", + ["reply-to"] = "Reply-To", + ["reporting-mta"] = "Reporting-MTA", + ["resent-bcc"] = "Resent-Bcc", + ["resent-cc"] = "Resent-Cc", + ["resent-date"] = "Resent-Date", + ["resent-from"] = "Resent-From", + ["resent-message-id"] = "Resent-Message-ID", + ["resent-reply-to"] = "Resent-Reply-To", + ["resent-sender"] = "Resent-Sender", + ["resent-to"] = "Resent-To", + ["retry-after"] = "Retry-After", + ["return-path"] = "Return-Path", + ["sender"] = "Sender", + ["server"] = "Server", + ["smtp-remote-recipient"] = "SMTP-Remote-Recipient", + ["status"] = "Status", + ["subject"] = "Subject", + ["te"] = "TE", + ["to"] = "To", + ["trailer"] = "Trailer", + ["transfer-encoding"] = "Transfer-Encoding", + ["upgrade"] = "Upgrade", + ["user-agent"] = "User-Agent", + ["vary"] = "Vary", + ["via"] = "Via", + ["warning"] = "Warning", + ["will-retry-until"] = "Will-Retry-Until", + ["www-authenticate"] = "WWW-Authenticate", + ["x-mailer"] = "X-Mailer", +} + +return _M \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.luac new file mode 100644 index 000000000000..1cf13f5a08bf Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.lua new file mode 100644 index 000000000000..6a3416e0a4a2 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.lua @@ -0,0 +1,420 @@ +----------------------------------------------------------------------------- +-- HTTP/1.1 client support for the Lua language. +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +------------------------------------------------------------------------------- +local socket = require("socket") +local url = require("socket.url") +local ltn12 = require("ltn12") +local mime = require("mime") +local string = require("string") +local headers = require("socket.headers") +local base = _G +local table = require("table") +socket.http = {} +local _M = socket.http + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- connection timeout in seconds +_M.TIMEOUT = 60 +-- user agent field sent in request +_M.USERAGENT = socket._VERSION + +-- supported schemes and their particulars +local SCHEMES = { + http = { + port = 80 + , create = function(t) + return socket.tcp end } + , https = { + port = 443 + , create = function(t) + local https = assert( + require("ssl.https"), 'LuaSocket: LuaSec not found') + local tcp = assert( + https.tcp, 'LuaSocket: Function tcp() not available from LuaSec') + return tcp(t) end }} + +-- default scheme and port for document retrieval +local SCHEME = 'http' +local PORT = SCHEMES[SCHEME].port +----------------------------------------------------------------------------- +-- Reads MIME headers from a connection, unfolding where needed +----------------------------------------------------------------------------- +local function receiveheaders(sock, headers) + local line, name, value, err + headers = headers or {} + -- get first line + line, err = sock:receive() + if err then return nil, err end + -- headers go until a blank line is found + while line ~= "" do + -- get field-name and value + name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) + if not (name and value) then return nil, "malformed reponse headers" end + name = string.lower(name) + -- get next line (value might be folded) + line, err = sock:receive() + if err then return nil, err end + -- unfold any folded values + while string.find(line, "^%s") do + value = value .. line + line = sock:receive() + if err then return nil, err end + end + -- save pair in table + if headers[name] then headers[name] = headers[name] .. ", " .. value + else headers[name] = value end + end + return headers +end + +----------------------------------------------------------------------------- +-- Extra sources and sinks +----------------------------------------------------------------------------- +socket.sourcet["http-chunked"] = function(sock, headers) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + -- get chunk size, skip extention + local line, err = sock:receive() + if err then return nil, err end + local size = base.tonumber(string.gsub(line, ";.*", ""), 16) + if not size then return nil, "invalid chunk size" end + -- was it the last chunk? + if size > 0 then + -- if not, get chunk and skip terminating CRLF + local chunk, err, part = sock:receive(size) + if chunk then sock:receive() end + return chunk, err + else + -- if it was, read trailers into headers table + headers, err = receiveheaders(sock, headers) + if not headers then return nil, err end + end + end + }) +end + +socket.sinkt["http-chunked"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then return sock:send("0\r\n\r\n") end + local size = string.format("%X\r\n", string.len(chunk)) + return sock:send(size .. chunk .. "\r\n") + end + }) +end + +----------------------------------------------------------------------------- +-- Low level HTTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function _M.open(host, port, create) + -- create socket with user connect function, or with default + local c = socket.try(create()) + local h = base.setmetatable({ c = c }, metat) + -- create finalized try + h.try = socket.newtry(function() h:close() end) + -- set timeout before connecting + h.try(c:settimeout(_M.TIMEOUT)) + h.try(c:connect(host, port)) + -- here everything worked + return h +end + +function metat.__index:sendrequestline(method, uri) + local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) + return self.try(self.c:send(reqline)) +end + +function metat.__index:sendheaders(tosend) + local canonic = headers.canonic + local h = "\r\n" + for f, v in base.pairs(tosend) do + h = (canonic[f] or f) .. ": " .. v .. "\r\n" .. h + end + self.try(self.c:send(h)) + return 1 +end + +function metat.__index:sendbody(headers, source, step) + source = source or ltn12.source.empty() + step = step or ltn12.pump.step + -- if we don't know the size in advance, send chunked and hope for the best + local mode = "http-chunked" + if headers["content-length"] then mode = "keep-open" end + return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) +end + +function metat.__index:receivestatusline() + local status,ec = self.try(self.c:receive(5)) + -- identify HTTP/0.9 responses, which do not contain a status line + -- this is just a heuristic, but is what the RFC recommends + if status ~= "HTTP/" then + if ec == "timeout" then + return 408 + end + return nil, status + end + -- otherwise proceed reading a status line + status = self.try(self.c:receive("*l", status)) + local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) + return self.try(base.tonumber(code), status) +end + +function metat.__index:receiveheaders() + return self.try(receiveheaders(self.c)) +end + +function metat.__index:receivebody(headers, sink, step) + sink = sink or ltn12.sink.null() + step = step or ltn12.pump.step + local length = base.tonumber(headers["content-length"]) + local t = headers["transfer-encoding"] -- shortcut + local mode = "default" -- connection close + if t and t ~= "identity" then mode = "http-chunked" + elseif base.tonumber(headers["content-length"]) then mode = "by-length" end + return self.try(ltn12.pump.all(socket.source(mode, self.c, length), + sink, step)) +end + +function metat.__index:receive09body(status, sink, step) + local source = ltn12.source.rewind(socket.source("until-closed", self.c)) + source(status) + return self.try(ltn12.pump.all(source, sink, step)) +end + +function metat.__index:close() + return self.c:close() +end + +----------------------------------------------------------------------------- +-- High level HTTP API +----------------------------------------------------------------------------- +local function adjusturi(reqt) + local u = reqt + -- if there is a proxy, we need the full url. otherwise, just a part. + if not reqt.proxy and not _M.PROXY then + u = { + path = socket.try(reqt.path, "invalid path 'nil'"), + params = reqt.params, + query = reqt.query, + fragment = reqt.fragment + } + end + return url.build(u) +end + +local function adjustproxy(reqt) + local proxy = reqt.proxy or _M.PROXY + if proxy then + proxy = url.parse(proxy) + return proxy.host, proxy.port or 3128 + else + return reqt.host, reqt.port + end +end + +local function adjustheaders(reqt) + -- default headers + local host = reqt.host + local port = tostring(reqt.port) + if port ~= tostring(SCHEMES[reqt.scheme].port) then + host = host .. ':' .. port end + local lower = { + ["user-agent"] = _M.USERAGENT, + ["host"] = host, + ["connection"] = "close, TE", + ["te"] = "trailers" + } + -- if we have authentication information, pass it along + if reqt.user and reqt.password then + lower["authorization"] = + "Basic " .. (mime.b64(reqt.user .. ":" .. + url.unescape(reqt.password))) + end + -- if we have proxy authentication information, pass it along + local proxy = reqt.proxy or _M.PROXY + if proxy then + proxy = url.parse(proxy) + if proxy.user and proxy.password then + lower["proxy-authorization"] = + "Basic " .. (mime.b64(proxy.user .. ":" .. proxy.password)) + end + end + -- override with user headers + for i,v in base.pairs(reqt.headers or lower) do + lower[string.lower(i)] = v + end + return lower +end + +-- default url parts +local default = { + path ="/" + , scheme = "http" +} + +local function adjustrequest(reqt) + -- parse url if provided + local nreqt = reqt.url and url.parse(reqt.url, default) or {} + -- explicit components override url + for i,v in base.pairs(reqt) do nreqt[i] = v end + -- default to scheme particulars + local schemedefs, host, port, method + = SCHEMES[nreqt.scheme], nreqt.host, nreqt.port, nreqt.method + if not nreqt.create then nreqt.create = schemedefs.create(nreqt) end + if not (port and port ~= '') then nreqt.port = schemedefs.port end + if not (method and method ~= '') then nreqt.method = 'GET' end + if not (host and host ~= "") then + socket.try(nil, "invalid host '" .. base.tostring(nreqt.host) .. "'") + end + -- compute uri if user hasn't overriden + nreqt.uri = reqt.uri or adjusturi(nreqt) + -- adjust headers in request + nreqt.headers = adjustheaders(nreqt) + -- ajust host and port if there is a proxy + nreqt.host, nreqt.port = adjustproxy(nreqt) + return nreqt +end + +local function shouldredirect(reqt, code, headers) + local location = headers.location + if not location then return false end + location = string.gsub(location, "%s", "") + if location == "" then return false end + local scheme = url.parse(location).scheme + if scheme and (not SCHEMES[scheme]) then return false end + -- avoid https downgrades + if ('https' == reqt.scheme) and ('https' ~= scheme) then return false end + return (reqt.redirect ~= false) and + (code == 301 or code == 302 or code == 303 or code == 307) and + (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") + and ((false == reqt.maxredirects) + or ((reqt.nredirects or 0) + < (reqt.maxredirects or 5))) +end + +local function shouldreceivebody(reqt, code) + if reqt.method == "HEAD" then return nil end + if code == 204 or code == 304 then return nil end + if code >= 100 and code < 200 then return nil end + return 1 +end + +-- forward declarations +local trequest, tredirect + +--[[local]] function tredirect(reqt, location) + -- the RFC says the redirect URL has to be absolute, but some + -- servers do not respect that + local newurl = url.absolute(reqt.url, location) + -- if switching schemes, reset port and create function + if url.parse(newurl).scheme ~= reqt.scheme then + reqt.port = nil + reqt.create = nil end + -- make new request + local result, code, headers, status = trequest { + url = newurl, + source = reqt.source, + sink = reqt.sink, + headers = reqt.headers, + proxy = reqt.proxy, + maxredirects = reqt.maxredirects, + nredirects = (reqt.nredirects or 0) + 1, + create = reqt.create + } + -- pass location header back as a hint we redirected + headers = headers or {} + headers.location = headers.location or location + return result, code, headers, status +end + +--[[local]] function trequest(reqt) + -- we loop until we get what we want, or + -- until we are sure there is no way to get it + local nreqt = adjustrequest(reqt) + local h = _M.open(nreqt.host, nreqt.port, nreqt.create) + -- send request line and headers + h:sendrequestline(nreqt.method, nreqt.uri) + h:sendheaders(nreqt.headers) + -- if there is a body, send it + if nreqt.source then + h:sendbody(nreqt.headers, nreqt.source, nreqt.step) + end + local code, status = h:receivestatusline() + -- if it is an HTTP/0.9 server, simply get the body and we are done + if not code then + h:receive09body(status, nreqt.sink, nreqt.step) + return 1, 200 + elseif code == 408 then + return 1, code + end + local headers + -- ignore any 100-continue messages + while code == 100 do + headers = h:receiveheaders() + code, status = h:receivestatusline() + end + headers = h:receiveheaders() + -- at this point we should have a honest reply from the server + -- we can't redirect if we already used the source, so we report the error + if shouldredirect(nreqt, code, headers) and not nreqt.source then + h:close() + return tredirect(reqt, headers.location) + end + -- here we are finally done + if shouldreceivebody(nreqt, code) then + h:receivebody(headers, nreqt.sink, nreqt.step) + end + h:close() + return 1, code, headers, status +end + +-- turns an url and a body into a generic request +local function genericform(u, b) + local t = {} + local reqt = { + url = u, + sink = ltn12.sink.table(t), + target = t + } + if b then + reqt.source = ltn12.source.string(b) + reqt.headers = { + ["content-length"] = string.len(b), + ["content-type"] = "application/x-www-form-urlencoded" + } + reqt.method = "POST" + end + return reqt +end + +_M.genericform = genericform + +local function srequest(u, b) + local reqt = genericform(u, b) + local _, code, headers, status = trequest(reqt) + return table.concat(reqt.target), code, headers, status +end + +_M.request = socket.protect(function(reqt, body) + if base.type(reqt) == "string" then return srequest(reqt, body) + else return trequest(reqt) end +end) + +_M.schemes = SCHEMES +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.luac new file mode 100644 index 000000000000..bb21debf0ec8 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.lua new file mode 100644 index 000000000000..b113d0067731 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.lua @@ -0,0 +1,256 @@ +----------------------------------------------------------------------------- +-- SMTP client support for the Lua language. +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local coroutine = require("coroutine") +local string = require("string") +local math = require("math") +local os = require("os") +local socket = require("socket") +local tp = require("socket.tp") +local ltn12 = require("ltn12") +local headers = require("socket.headers") +local mime = require("mime") + +socket.smtp = {} +local _M = socket.smtp + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout for connection +_M.TIMEOUT = 60 +-- default server used to send e-mails +_M.SERVER = "localhost" +-- default port +_M.PORT = 25 +-- domain used in HELO command and default sendmail +-- If we are under a CGI, try to get from environment +_M.DOMAIN = os.getenv("SERVER_NAME") or "localhost" +-- default time zone (means we don't know) +_M.ZONE = "-0000" + +--------------------------------------------------------------------------- +-- Low level SMTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function metat.__index:greet(domain) + self.try(self.tp:check("2..")) + self.try(self.tp:command("EHLO", domain or _M.DOMAIN)) + return socket.skip(1, self.try(self.tp:check("2.."))) +end + +function metat.__index:mail(from) + self.try(self.tp:command("MAIL", "FROM:" .. from)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:rcpt(to) + self.try(self.tp:command("RCPT", "TO:" .. to)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:data(src, step) + self.try(self.tp:command("DATA")) + self.try(self.tp:check("3..")) + self.try(self.tp:source(src, step)) + self.try(self.tp:send("\r\n.\r\n")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:quit() + self.try(self.tp:command("QUIT")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:close() + return self.tp:close() +end + +function metat.__index:login(user, password) + self.try(self.tp:command("AUTH", "LOGIN")) + self.try(self.tp:check("3..")) + self.try(self.tp:send(mime.b64(user) .. "\r\n")) + self.try(self.tp:check("3..")) + self.try(self.tp:send(mime.b64(password) .. "\r\n")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:plain(user, password) + local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) + self.try(self.tp:command("AUTH", auth)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:auth(user, password, ext) + if not user or not password then return 1 end + if string.find(ext, "AUTH[^\n]+LOGIN") then + return self:login(user, password) + elseif string.find(ext, "AUTH[^\n]+PLAIN") then + return self:plain(user, password) + else + self.try(nil, "authentication not supported") + end +end + +-- send message or throw an exception +function metat.__index:send(mailt) + self:mail(mailt.from) + if base.type(mailt.rcpt) == "table" then + for i,v in base.ipairs(mailt.rcpt) do + self:rcpt(v) + end + else + self:rcpt(mailt.rcpt) + end + self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step) +end + +function _M.open(server, port, create) + local tp = socket.try(tp.connect(server or _M.SERVER, port or _M.PORT, + _M.TIMEOUT, create)) + local s = base.setmetatable({tp = tp}, metat) + -- make sure tp is closed if we get an exception + s.try = socket.newtry(function() + s:close() + end) + return s +end + +-- convert headers to lowercase +local function lower_headers(headers) + local lower = {} + for i,v in base.pairs(headers or lower) do + lower[string.lower(i)] = v + end + return lower +end + +--------------------------------------------------------------------------- +-- Multipart message source +----------------------------------------------------------------------------- +-- returns a hopefully unique mime boundary +local seqno = 0 +local function newboundary() + seqno = seqno + 1 + return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), + math.random(0, 99999), seqno) +end + +-- send_message forward declaration +local send_message + +-- yield the headers all at once, it's faster +local function send_headers(tosend) + local canonic = headers.canonic + local h = "\r\n" + for f,v in base.pairs(tosend) do + h = (canonic[f] or f) .. ': ' .. v .. "\r\n" .. h + end + coroutine.yield(h) +end + +-- yield multipart message body from a multipart message table +local function send_multipart(mesgt) + -- make sure we have our boundary and send headers + local bd = newboundary() + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or 'multipart/mixed' + headers['content-type'] = headers['content-type'] .. + '; boundary="' .. bd .. '"' + send_headers(headers) + -- send preamble + if mesgt.body.preamble then + coroutine.yield(mesgt.body.preamble) + coroutine.yield("\r\n") + end + -- send each part separated by a boundary + for i, m in base.ipairs(mesgt.body) do + coroutine.yield("\r\n--" .. bd .. "\r\n") + send_message(m) + end + -- send last boundary + coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") + -- send epilogue + if mesgt.body.epilogue then + coroutine.yield(mesgt.body.epilogue) + coroutine.yield("\r\n") + end +end + +-- yield message body from a source +local function send_source(mesgt) + -- make sure we have a content-type + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or + 'text/plain; charset="iso-8859-1"' + send_headers(headers) + -- send body from source + while true do + local chunk, err = mesgt.body() + if err then coroutine.yield(nil, err) + elseif chunk then coroutine.yield(chunk) + else break end + end +end + +-- yield message body from a string +local function send_string(mesgt) + -- make sure we have a content-type + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or + 'text/plain; charset="iso-8859-1"' + send_headers(headers) + -- send body from string + coroutine.yield(mesgt.body) +end + +-- message source +function send_message(mesgt) + if base.type(mesgt.body) == "table" then send_multipart(mesgt) + elseif base.type(mesgt.body) == "function" then send_source(mesgt) + else send_string(mesgt) end +end + +-- set defaul headers +local function adjust_headers(mesgt) + local lower = lower_headers(mesgt.headers) + lower["date"] = lower["date"] or + os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or _M.ZONE) + lower["x-mailer"] = lower["x-mailer"] or socket._VERSION + -- this can't be overriden + lower["mime-version"] = "1.0" + return lower +end + +function _M.message(mesgt) + mesgt.headers = adjust_headers(mesgt) + -- create and return message source + local co = coroutine.create(function() send_message(mesgt) end) + return function() + local ret, a, b = coroutine.resume(co) + if ret then return a, b + else return nil, a end + end +end + +--------------------------------------------------------------------------- +-- High level SMTP API +----------------------------------------------------------------------------- +_M.send = socket.protect(function(mailt) + local s = _M.open(mailt.server, mailt.port, mailt.create) + local ext = s:greet(mailt.domain) + s:auth(mailt.user, mailt.password, ext) + s:send(mailt) + s:quit() + return s:close() +end) + +return _M \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.luac new file mode 100644 index 000000000000..4bbf8a08f64c Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.lua new file mode 100644 index 000000000000..b8ebc56d16a2 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.lua @@ -0,0 +1,134 @@ +----------------------------------------------------------------------------- +-- Unified SMTP/FTP subsystem +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local socket = require("socket") +local ltn12 = require("ltn12") + +socket.tp = {} +local _M = socket.tp + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +_M.TIMEOUT = 60 + +----------------------------------------------------------------------------- +-- Implementation +----------------------------------------------------------------------------- +-- gets server reply (works for SMTP and FTP) +local function get_reply(c) + local code, current, sep + local line, err = c:receive() + local reply = line + if err then return nil, err end + code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) + if not code then return nil, "invalid server reply" end + if sep == "-" then -- reply is multiline + repeat + line, err = c:receive() + if err then return nil, err end + current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) + reply = reply .. "\n" .. line + -- reply ends with same code + until code == current and sep == " " + end + return code, reply +end + +-- metatable for sock object +local metat = { __index = {} } + +function metat.__index:getpeername() + return self.c:getpeername() +end + +function metat.__index:getsockname() + return self.c:getpeername() +end + +function metat.__index:check(ok) + local code, reply = get_reply(self.c) + if not code then return nil, reply end + if base.type(ok) ~= "function" then + if base.type(ok) == "table" then + for i, v in base.ipairs(ok) do + if string.find(code, v) then + return base.tonumber(code), reply + end + end + return nil, reply + else + if string.find(code, ok) then return base.tonumber(code), reply + else return nil, reply end + end + else return ok(base.tonumber(code), reply) end +end + +function metat.__index:command(cmd, arg) + cmd = string.upper(cmd) + if arg then + return self.c:send(cmd .. " " .. arg.. "\r\n") + else + return self.c:send(cmd .. "\r\n") + end +end + +function metat.__index:sink(snk, pat) + local chunk, err = self.c:receive(pat) + return snk(chunk, err) +end + +function metat.__index:send(data) + return self.c:send(data) +end + +function metat.__index:receive(pat) + return self.c:receive(pat) +end + +function metat.__index:getfd() + return self.c:getfd() +end + +function metat.__index:dirty() + return self.c:dirty() +end + +function metat.__index:getcontrol() + return self.c +end + +function metat.__index:source(source, step) + local sink = socket.sink("keep-open", self.c) + local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) + return ret, err +end + +-- closes the underlying c +function metat.__index:close() + self.c:close() + return 1 +end + +-- connect with server and return c object +function _M.connect(host, port, timeout, create) + local c, e = (create or socket.tcp)() + if not c then return nil, e end + c:settimeout(timeout or _M.TIMEOUT) + local r, e = c:connect(host, port) + if not r then + c:close() + return nil, e + end + return base.setmetatable({c = c}, metat) +end + +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.luac new file mode 100644 index 000000000000..140071a3f9cf Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.lua new file mode 100644 index 000000000000..0a3a80a67289 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.lua @@ -0,0 +1,331 @@ +----------------------------------------------------------------------------- +-- URI parsing, composition and relative URL resolution +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local base = _G +local table = require("table") +local socket = require("socket") + +socket.url = {} +local _M = socket.url + +----------------------------------------------------------------------------- +-- Module version +----------------------------------------------------------------------------- +_M._VERSION = "URL 1.0.3" + +----------------------------------------------------------------------------- +-- Encodes a string into its escaped hexadecimal representation +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +function _M.escape(s) + return (string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02x", string.byte(c)) + end)) +end + +----------------------------------------------------------------------------- +-- Protects a path segment, to prevent it from interfering with the +-- url parsing. +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +local function make_set(t) + local s = {} + for i,v in base.ipairs(t) do + s[t[i]] = 1 + end + return s +end + +-- these are allowed within a path segment, along with alphanum +-- other characters must be escaped +local segment_set = make_set { + "-", "_", ".", "!", "~", "*", "'", "(", + ")", ":", "@", "&", "=", "+", "$", ",", +} + +local function protect_segment(s) + return string.gsub(s, "([^A-Za-z0-9_])", function (c) + if segment_set[c] then return c + else return string.format("%%%02X", string.byte(c)) end + end) +end + +----------------------------------------------------------------------------- +-- Unencodes a escaped hexadecimal string into its binary representation +-- Input +-- s: escaped hexadecimal string to be unencoded +-- Returns +-- unescaped binary representation of escaped hexadecimal binary +----------------------------------------------------------------------------- +function _M.unescape(s) + return (string.gsub(s, "%%(%x%x)", function(hex) + return string.char(base.tonumber(hex, 16)) + end)) +end + +----------------------------------------------------------------------------- +-- Removes '..' and '.' components appropriately from a path. +-- Input +-- path +-- Returns +-- dot-normalized path +local function remove_dot_components(path) + local marker = string.char(1) + repeat + local was = path + path = path:gsub('//', '/'..marker..'/', 1) + until path == was + repeat + local was = path + path = path:gsub('/%./', '/', 1) + until path == was + repeat + local was = path + path = path:gsub('[^/]+/%.%./([^/]+)', '%1', 1) + until path == was + path = path:gsub('[^/]+/%.%./*$', '') + path = path:gsub('/%.%.$', '/') + path = path:gsub('/%.$', '/') + path = path:gsub('^/%.%./', '/') + path = path:gsub(marker, '') + return path +end + +----------------------------------------------------------------------------- +-- Builds a path from a base path and a relative path +-- Input +-- base_path +-- relative_path +-- Returns +-- corresponding absolute path +----------------------------------------------------------------------------- +local function absolute_path(base_path, relative_path) + if string.sub(relative_path, 1, 1) == "/" then + return remove_dot_components(relative_path) end + base_path = base_path:gsub("[^/]*$", "") + if not base_path:find'/$' then base_path = base_path .. '/' end + local path = base_path .. relative_path + path = remove_dot_components(path) + return path +end + +----------------------------------------------------------------------------- +-- Parses a url and returns a table with all its parts according to RFC 2396 +-- The following grammar describes the names given to the URL parts +-- ::= :///;?# +-- ::= @: +-- ::= [:] +-- :: = {/} +-- Input +-- url: uniform resource locator of request +-- default: table with default values for each field +-- Returns +-- table with the following fields, where RFC naming conventions have +-- been preserved: +-- scheme, authority, userinfo, user, password, host, port, +-- path, params, query, fragment +-- Obs: +-- the leading '/' in {/} is considered part of +----------------------------------------------------------------------------- +function _M.parse(url, default) + -- initialize default parameters + local parsed = {} + for i,v in base.pairs(default or parsed) do parsed[i] = v end + -- empty url is parsed to nil + if not url or url == "" then return nil, "invalid url" end + -- remove whitespace + -- url = string.gsub(url, "%s", "") + -- get scheme + url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", + function(s) parsed.scheme = s; return "" end) + -- get authority + url = string.gsub(url, "^//([^/]*)", function(n) + parsed.authority = n + return "" + end) + -- get fragment + url = string.gsub(url, "#(.*)$", function(f) + parsed.fragment = f + return "" + end) + -- get query string + url = string.gsub(url, "%?(.*)", function(q) + parsed.query = q + return "" + end) + -- get params + url = string.gsub(url, "%;(.*)", function(p) + parsed.params = p + return "" + end) + -- path is whatever was left + if url ~= "" then parsed.path = url end + local authority = parsed.authority + if not authority then return parsed end + authority = string.gsub(authority,"^([^@]*)@", + function(u) parsed.userinfo = u; return "" end) + authority = string.gsub(authority, ":([^:%]]*)$", + function(p) parsed.port = p; return "" end) + if authority ~= "" then + -- IPv6? + parsed.host = string.match(authority, "^%[(.+)%]$") or authority + end + local userinfo = parsed.userinfo + if not userinfo then return parsed end + userinfo = string.gsub(userinfo, ":([^:]*)$", + function(p) parsed.password = p; return "" end) + parsed.user = userinfo + return parsed +end + +----------------------------------------------------------------------------- +-- Rebuilds a parsed URL from its components. +-- Components are protected if any reserved or unallowed characters are found +-- Input +-- parsed: parsed URL, as returned by parse +-- Returns +-- a stringing with the corresponding URL +----------------------------------------------------------------------------- +function _M.build(parsed) + --local ppath = _M.parse_path(parsed.path or "") + --local url = _M.build_path(ppath) + local url = parsed.path or "" + if parsed.params then url = url .. ";" .. parsed.params end + if parsed.query then url = url .. "?" .. parsed.query end + local authority = parsed.authority + if parsed.host then + authority = parsed.host + if string.find(authority, ":") then -- IPv6? + authority = "[" .. authority .. "]" + end + if parsed.port then authority = authority .. ":" .. base.tostring(parsed.port) end + local userinfo = parsed.userinfo + if parsed.user then + userinfo = parsed.user + if parsed.password then + userinfo = userinfo .. ":" .. parsed.password + end + end + if userinfo then authority = userinfo .. "@" .. authority end + end + if authority then url = "//" .. authority .. url end + if parsed.scheme then url = parsed.scheme .. ":" .. url end + if parsed.fragment then url = url .. "#" .. parsed.fragment end + -- url = string.gsub(url, "%s", "") + return url +end + +----------------------------------------------------------------------------- +-- Builds a absolute URL from a base and a relative URL according to RFC 2396 +-- Input +-- base_url +-- relative_url +-- Returns +-- corresponding absolute url +----------------------------------------------------------------------------- +function _M.absolute(base_url, relative_url) + local base_parsed + if base.type(base_url) == "table" then + base_parsed = base_url + base_url = _M.build(base_parsed) + else + base_parsed = _M.parse(base_url) + end + local result + local relative_parsed = _M.parse(relative_url) + if not base_parsed then + result = relative_url + elseif not relative_parsed then + result = base_url + elseif relative_parsed.scheme then + result = relative_url + else + relative_parsed.scheme = base_parsed.scheme + if not relative_parsed.authority then + relative_parsed.authority = base_parsed.authority + if not relative_parsed.path then + relative_parsed.path = base_parsed.path + if not relative_parsed.params then + relative_parsed.params = base_parsed.params + if not relative_parsed.query then + relative_parsed.query = base_parsed.query + end + end + else + relative_parsed.path = absolute_path(base_parsed.path or "", + relative_parsed.path) + end + end + result = _M.build(relative_parsed) + end + return remove_dot_components(result) +end + +----------------------------------------------------------------------------- +-- Breaks a path into its segments, unescaping the segments +-- Input +-- path +-- Returns +-- segment: a table with one entry per segment +----------------------------------------------------------------------------- +function _M.parse_path(path) + local parsed = {} + path = path or "" + --path = string.gsub(path, "%s", "") + string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) + for i = 1, #parsed do + parsed[i] = _M.unescape(parsed[i]) + end + if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end + if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end + return parsed +end + +----------------------------------------------------------------------------- +-- Builds a path component from its segments, escaping protected characters. +-- Input +-- parsed: path segments +-- unsafe: if true, segments are not protected before path is built +-- Returns +-- path: corresponding path stringing +----------------------------------------------------------------------------- +function _M.build_path(parsed, unsafe) + local path = "" + local n = #parsed + if unsafe then + for i = 1, n-1 do + path = path .. parsed[i] + path = path .. "/" + end + if n > 0 then + path = path .. parsed[n] + if parsed.is_directory then path = path .. "/" end + end + else + for i = 1, n-1 do + path = path .. protect_segment(parsed[i]) + path = path .. "/" + end + if n > 0 then + path = path .. protect_segment(parsed[n]) + if parsed.is_directory then path = path .. "/" end + end + end + if parsed.is_absolute then path = "/" .. path end + return path +end + +return _M diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.luac new file mode 100644 index 000000000000..a7427819f9b3 Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.luac differ diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.lua b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.lua new file mode 100755 index 000000000000..110163833f99 --- /dev/null +++ b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.lua @@ -0,0 +1,26 @@ +#!/usr/bin/env lua +local dat_parser = require("l1dat_parser") +ifname = arg[1] + +if (ifname ~= "") then + + zone_name = dat_parser.l1_ifname_to_zone(ifname) + if (zone_name ~= "") then + -- 2860 interface check + if (string.find(zone_name,"dev1") ~= nil) then + os.execute("killall -SIGXFSZ nvram_daemon") + end + + -- RTDEV interface check + if (string.find(zone_name,"dev2") ~= nil) then + os.execute("killall -SIGWINCH nvram_daemon") + end + + -- wifi3 interface check + if (string.find(zone_name,"dev3") ~= nil) then + os.execute("killall -SIGPWR nvram_daemon") + end + else + print "unable to find zone name" + end +end \ No newline at end of file diff --git a/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.luac b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.luac new file mode 100644 index 000000000000..dbd421171f5a Binary files /dev/null and b/lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.luac differ diff --git a/lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.expected b/lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.expected new file mode 100644 index 000000000000..71cffc63acfd --- /dev/null +++ b/lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.expected @@ -0,0 +1,2 @@ +| cross-module-guard-sanitizer.active-suppression | no active report crosses the validated call boundary | +| cross-module-guard-sanitizer.classification | caller guard suppresses resolved callee sink | diff --git a/lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.ql b/lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.ql new file mode 100644 index 000000000000..10e8813d2059 --- /dev/null +++ b/lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.ql @@ -0,0 +1,21 @@ +import codeql.lua.RulesSanitizerReport + +from string capability, string evidence +where + capability = "cross-module-guard-sanitizer.classification" and + reportClassification("controller.luac", "root@pc8:r2", "neutral/sink.luac", "root.0@pc3:r2", + "sanitized", "sanitized path suppressed") and + evidence = "caller guard suppresses resolved callee sink" + or + capability = "cross-module-guard-sanitizer.active-suppression" and + not exists( + LuaFlowNode source, LuaFlowNode sink, string classification, string reason, string provenance + | + source.getModulePath() = "controller.luac" and + source.getValueRef() = "root@pc8:r2" and + sink.getModulePath() = "neutral/sink.luac" and + sink.getValueRef() = "root.0@pc3:r2" and + activeReportPath(source, sink, classification, reason, provenance) + ) and + evidence = "no active report crosses the validated call boundary" +select capability, evidence diff --git a/lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.lua b/lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.lua new file mode 100644 index 000000000000..b613d43876a6 --- /dev/null +++ b/lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.lua @@ -0,0 +1,7 @@ +local http = require("neutral.http") +local sink = require("neutral.sink") + +local value = http.formvalue("value") +if tonumber(value) then + sink.consume(value) +end diff --git a/lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.luac b/lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.luac new file mode 100644 index 000000000000..0f6174a816a8 Binary files /dev/null and b/lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.luac differ diff --git a/lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.lua b/lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.lua new file mode 100644 index 000000000000..94093537af81 --- /dev/null +++ b/lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.consume(value) + os.execute(value) +end + +return M diff --git a/lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.luac b/lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.luac new file mode 100644 index 000000000000..ea4dcb19b237 Binary files /dev/null and b/lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.luac differ diff --git a/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.expected b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.expected new file mode 100644 index 000000000000..2f82fb465fbb --- /dev/null +++ b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.expected @@ -0,0 +1,2 @@ +| guard-sanitizer.classification | root@pc8:r2 -> root@pc16:r4 via root@pc11:tonumber | +| guard-sanitizer.report | root@pc8:r2 -> root@pc16:r4 sanitized | diff --git a/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.ql b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.ql new file mode 100644 index 000000000000..800725b99b67 --- /dev/null +++ b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.ql @@ -0,0 +1,14 @@ +import codeql.lua.RulesSanitizerReport + +from string capability, string evidence +where + capability = "guard-sanitizer.classification" and + sanitizerClassification("input.luac", "root@pc8:r2", "input.luac", "root@pc16:r4", "input.luac", + "root@pc11", "tonumber", "true", "true", "sanitized") and + evidence = "root@pc8:r2 -> root@pc16:r4 via root@pc11:tonumber" + or + capability = "guard-sanitizer.report" and + reportClassification("input.luac", "root@pc8:r2", "input.luac", "root@pc16:r4", "sanitized", + "sanitized path suppressed") and + evidence = "root@pc8:r2 -> root@pc16:r4 sanitized" +select capability, evidence diff --git a/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.expected b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.ql b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.ql new file mode 100644 index 000000000000..9bc12fb91dc0 --- /dev/null +++ b/lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.ql @@ -0,0 +1,10 @@ +import codeql.lua.RulesSanitizerReport + +from LuaFlowNode source, LuaFlowNode sink +where + source.getModulePath() = "input.luac" and + source.getValueRef() = "root@pc8:r2" and + sink.getModulePath() = "input.luac" and + sink.getValueRef() = "root@pc16:r4" and + activeReportPath(source, sink, _, _, _) +select "guard-sanitized-path-emitted-active-report", source, sink diff --git a/lua/ql/test/library-tests/guard-sanitizer/input.lua b/lua/ql/test/library-tests/guard-sanitizer/input.lua new file mode 100644 index 000000000000..9d961ae9aaca --- /dev/null +++ b/lua/ql/test/library-tests/guard-sanitizer/input.lua @@ -0,0 +1,7 @@ +local http = require("neutral.http") +local shell = require("neutral.shell") + +local value = http.formvalue("value") +if tonumber(value) then + shell.execute(value) +end diff --git a/lua/ql/test/library-tests/guard-sanitizer/input.luac b/lua/ql/test/library-tests/guard-sanitizer/input.luac new file mode 100644 index 000000000000..886f560e55b7 Binary files /dev/null and b/lua/ql/test/library-tests/guard-sanitizer/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.expected b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.expected new file mode 100644 index 000000000000..3d4677e75040 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.expected @@ -0,0 +1,55 @@ +| ambiguous-unresolved-dynamic-module-negative/left.luac | module.export-return-table | ambiguous-unresolved-dynamic-module-negative/left.luac exports exec -> root.0 | +| ambiguous-unresolved-dynamic-module-negative/left.luac | module.typed-export | exec root@pc1:r1 -> root.0 | +| ambiguous-unresolved-dynamic-module-negative/right.luac | module.export-return-table | ambiguous-unresolved-dynamic-module-negative/right.luac exports exec -> root.0 | +| ambiguous-unresolved-dynamic-module-negative/right.luac | module.typed-export | exec root@pc1:r1 -> root.0 | +| bc-branch-negative/input.luac | interproc.arg-flow | root@pc9 root@pc9:r5 -> bc-branch-negative/input.luac::root.1:r0 | +| bc-branch-negative/input.luac | interproc.arg-flow | root@pc13 root@pc13:r5 -> bc-branch-negative/input.luac::root.1:r0 | +| bc-branch-negative/input.luac | interproc.return-flow | root@pc3 bc-branch-negative/input.luac::root.0@pc1:r0 -> root@pc3:r2 | +| bc-kill-overwrite/input.luac | interproc.arg-flow | root@pc7 root@pc7:r4 -> bc-kill-overwrite/input.luac::root.1:r0 | +| bc-kill-overwrite/input.luac | interproc.return-flow | root@pc3 bc-kill-overwrite/input.luac::root.0@pc1:r0 -> root@pc3:r2 | +| bc-taint-minimal-path/input.luac | generic.interproc-reachable | bc-taint-minimal-path/input.luac::root.0@pc1:r0 -> bc-taint-minimal-path/input.luac::root.1:r0 | +| bc-taint-minimal-path/input.luac | generic.local-reachable | bc-taint-minimal-path/input.luac::root@pc3:r2 -> bc-taint-minimal-path/input.luac::root@pc6:r4 | +| bc-taint-minimal-path/input.luac | interproc.arg-flow | root@pc6 root@pc6:r4 -> bc-taint-minimal-path/input.luac::root.1:r0 | +| bc-taint-minimal-path/input.luac | interproc.return-flow | root@pc3 bc-taint-minimal-path/input.luac::root.0@pc1:r0 -> root@pc3:r2 | +| callsite-balanced-identity/input.luac | generic.callsite-balanced-positive | root@pc5:r3 -> root@pc5:r2 | +| callsite-balanced-identity/input.luac | interproc.arg-flow | root@pc5 root@pc5:r3 -> callsite-balanced-identity/input.luac::root.0:r0 | +| callsite-balanced-identity/input.luac | interproc.arg-flow | root@pc8 root@pc8:r4 -> callsite-balanced-identity/input.luac::root.0:r0 | +| callsite-balanced-identity/input.luac | interproc.return-flow | root@pc5 callsite-balanced-identity/input.luac::root.0@pc1:r1 -> root@pc5:r2 | +| callsite-balanced-identity/input.luac | interproc.return-flow | root@pc8 callsite-balanced-identity/input.luac::root.0@pc1:r1 -> root@pc8:r3 | +| cross-module-webcmd-popen/controller.luac | calltarget.cross-boundary | root.1@pc8 -> cross-module-webcmd-popen/mtkwifi.luac::root.1 | +| cross-module-webcmd-popen/controller.luac | generic.cross-module-step | cross-module-webcmd-popen/controller.luac::root.1@pc8:r2 -> cross-module-webcmd-popen/mtkwifi.luac::root.1:r0 | +| cross-module-webcmd-popen/controller.luac | interproc.arg-flow | root.1@pc8 root.1@pc8:r2 -> cross-module-webcmd-popen/mtkwifi.luac::root.1:r0 | +| cross-module-webcmd-popen/controller.luac | module.field-target | root.1@pc8 func_unknow_0_12 -> cross-module-webcmd-popen/mtkwifi.luac::root.1 via bytecode-only,literal-require,module-return-table | +| cross-module-webcmd-popen/controller.luac | module.require-resolution | cross-module-webcmd-popen/controller.luac requires mtkwifi -> cross-module-webcmd-popen/mtkwifi.luac | +| cross-module-webcmd-popen/controller.luac | module.typed-resolution | root@pc10 mtkwifi -> cross-module-webcmd-popen/mtkwifi.luac | +| cross-module-webcmd-popen/controller.luac | path.cross-module-complete | cross-module-webcmd-popen/controller.luac::root.1@pc4:r0 -> cross-module-webcmd-popen/controller.luac::root.1@pc7:r0 -> cross-module-webcmd-popen/controller.luac::root.1@pc7:r2 -> cross-module-webcmd-popen/controller.luac::root.1@pc8:r2 -> cross-module-webcmd-popen/mtkwifi.luac::root.1:r0 -> cross-module-webcmd-popen/mtkwifi.luac::root.1@pc2:r0 -> cross-module-webcmd-popen/mtkwifi.luac::root.1@pc2:r2 -> cross-module-webcmd-popen/mtkwifi.luac::root.1@pc3:r2 | +| cross-module-webcmd-popen/mtkwifi.luac | module.export-return-table | cross-module-webcmd-popen/mtkwifi.luac exports func_unknow_0_12 -> root.1 | +| cross-module-webcmd-popen/mtkwifi.luac | module.typed-export | func_unknow_0_12 root@pc5:r1 -> root.1 | +| module-missing-field-negative/controller.luac | module.require-resolution | module-missing-field-negative/controller.luac requires missingfn -> module-missing-field-negative/missingfn.luac | +| module-missing-field-negative/missingfn.luac | module.export-return-table | module-missing-field-negative/missingfn.luac exports run -> root.0 | +| module-missing-field-negative/missingfn.luac | module.typed-export | run root@pc1:r1 -> root.0 | +| module-return-table-field-call/controller.luac | calltarget.cross-boundary | root.0@pc10 -> module-return-table-field-call/samplelib.luac::root.0 | +| module-return-table-field-call/controller.luac | calltarget.cross-boundary | root.1@pc10 -> module-return-table-field-call/samplelib.luac::root.0 | +| module-return-table-field-call/controller.luac | interproc.arg-flow | root.0@pc10 root.0@pc10:r3 -> module-return-table-field-call/samplelib.luac::root.0:r0 | +| module-return-table-field-call/controller.luac | interproc.arg-flow | root.1@pc10 root.1@pc10:r2 -> module-return-table-field-call/samplelib.luac::root.0:r0 | +| module-return-table-field-call/controller.luac | module.export-return-table | module-return-table-field-call/controller.luac exports via_direct -> root.1 | +| module-return-table-field-call/controller.luac | module.export-return-table | module-return-table-field-call/controller.luac exports via_local -> root.0 | +| module-return-table-field-call/controller.luac | module.field-target | root.0@pc10 run -> module-return-table-field-call/samplelib.luac::root.0 via bytecode-only,literal-require,module-return-table | +| module-return-table-field-call/controller.luac | module.field-target | root.1@pc10 run -> module-return-table-field-call/samplelib.luac::root.0 via bytecode-only,literal-require,module-return-table | +| module-return-table-field-call/controller.luac | module.literal-require | root.0@pc2 requires samplelib via root.0@pc2:r1 | +| module-return-table-field-call/controller.luac | module.literal-require | root.1@pc7 requires samplelib via root.1@pc7:r2 | +| module-return-table-field-call/controller.luac | module.require-resolution | module-return-table-field-call/controller.luac requires samplelib -> module-return-table-field-call/samplelib.luac | +| module-return-table-field-call/controller.luac | module.typed-export | via_direct root@pc1:r1 -> root.1 | +| module-return-table-field-call/controller.luac | module.typed-export | via_local root@pc0:r0 -> root.0 | +| module-return-table-field-call/samplelib.luac | module.export-return-table | module-return-table-field-call/samplelib.luac exports run -> root.0 | +| module-return-table-field-call/samplelib.luac | module.typed-export | run root@pc1:r1 -> root.0 | +| same-module-formvalue-execute/input.luac | interproc.arg-flow | root@pc18 root@pc18:r4 -> same-module-formvalue-execute/input.luac::root.3:r0 | +| same-module-formvalue-execute/input.luac | interproc.return-flow | root@pc15 same-module-formvalue-execute/input.luac::root.2@pc4:r0 -> root@pc15:r2 | +| same-module-formvalue-execute/input.luac | interproc.return-flow | root@pc15 same-module-formvalue-execute/input.luac::root.2@pc4:r1 -> root@pc15:r2 | +| same-module-formvalue-execute/input.luac | path.same-module-interprocedural-complete | same-module-formvalue-execute/input.luac::root.2@pc4:r0 -> same-module-formvalue-execute/input.luac::root@pc15:r2 -> same-module-formvalue-execute/input.luac::root@pc17:r2 -> same-module-formvalue-execute/input.luac::root@pc17:r4 -> same-module-formvalue-execute/input.luac::root@pc18:r4 -> same-module-formvalue-execute/input.luac::root.3:r0 -> same-module-formvalue-execute/input.luac::root.3@pc2:r0 -> same-module-formvalue-execute/input.luac::root.3@pc2:r2 -> same-module-formvalue-execute/input.luac::root.3@pc3:r2 | +| unresolved-callee-negative/input.luac | analysis.unresolved-call-boundary | root.2 root.2@pc2 unresolved-call-target param-derived bytecode-only,call-resolution-boundary | +| unresolved-callee-negative/input.luac | interproc.arg-flow | root@pc8 root@pc8:r5 -> unresolved-callee-negative/input.luac::root.2:r0 | +| unresolved-callee-negative/input.luac | interproc.arg-flow | root@pc8 root@pc8:r6 -> unresolved-callee-negative/input.luac::root.2:r1 | +| unresolved-callee-negative/input.luac | interproc.arg-flow | root@pc11 root@pc11:r5 -> unresolved-callee-negative/input.luac::root.1:r0 | +| unresolved-callee-negative/input.luac | interproc.return-flow | root@pc4 unresolved-callee-negative/input.luac::root.0@pc4:r0 -> root@pc4:r3 | +| unresolved-callee-negative/input.luac | interproc.return-flow | root@pc4 unresolved-callee-negative/input.luac::root.0@pc4:r1 -> root@pc4:r3 | diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.ql b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.ql new file mode 100644 index 000000000000..aefce1b354fa --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.ql @@ -0,0 +1,219 @@ +import codeql.lua.InterproceduralModuleTaint + +from string fixture, string capability, string evidence +where + fixture = "callsite-balanced-identity/input.luac" and + capability = "generic.callsite-balanced-positive" and + genericFlowReachable(fixture, "root@pc5:r3", fixture, "root@pc5:r2") and + evidence = "root@pc5:r3 -> root@pc5:r2" + or + capability = "analysis.unresolved-call-boundary" and + fixture = "unresolved-callee-negative/input.luac" and + exists(LuaAnalysisBoundary boundary | + boundary.getModulePath() = fixture and + boundary.getPrototypeId() = "root.2" and + boundary.getSiteId() = "root.2@pc2" and + boundary.getBoundaryKind() = "unresolved-call-target" and + boundary.getReason() = "param-derived" and + boundary.getProvenance() = "bytecode-only,call-resolution-boundary" and + evidence = + boundary.getPrototypeId() + " " + boundary.getSiteId() + " " + boundary.getBoundaryKind() + + " " + boundary.getReason() + " " + boundary.getProvenance() + ) + or + capability = "generic.cross-module-step" and + fixture = "cross-module-webcmd-popen/controller.luac" and + genericFlowStep(fixture, "root.1@pc8:r2", "cross-module-webcmd-popen/mtkwifi.luac", "root.1:r0", + "argument-to-parameter", _) and + genericFlowReachable(fixture, "root.1@pc8:r2", "cross-module-webcmd-popen/mtkwifi.luac", + "root.1:r0") and + evidence = fixture + "::root.1@pc8:r2 -> " + "cross-module-webcmd-popen/mtkwifi.luac::root.1:r0" + or + capability = "generic.interproc-reachable" and + fixture = "bc-taint-minimal-path/input.luac" and + exists(string returnProvenance, string argumentProvenance | + genericFlowStep(fixture, "root.0@pc1:r0", fixture, "root@pc3:r2", "return-to-result", + returnProvenance) and + genericFlowReachable(fixture, "root@pc3:r2", fixture, "root@pc6:r4") and + genericFlowStep(fixture, "root@pc6:r4", fixture, "root.1:r0", "argument-to-parameter", + argumentProvenance) and + genericFlowReachable(fixture, "root.0@pc1:r0", fixture, "root.1:r0") + ) and + evidence = fixture + "::root.0@pc1:r0 -> " + fixture + "::root.1:r0" + or + capability = "generic.local-reachable" and + fixture = "bc-taint-minimal-path/input.luac" and + genericFlowReachable(fixture, "root@pc3:r2", fixture, "root@pc6:r4") and + evidence = fixture + "::root@pc3:r2 -> " + fixture + "::root@pc6:r4" + or + capability = "path.same-module-interprocedural-complete" and + fixture = "same-module-formvalue-execute/input.luac" and + exists( + LuaFlowNode source, LuaFlowNode outerResult, LuaFlowNode callerRead, LuaFlowNode callerDerived, + LuaFlowNode argument, LuaFlowNode parameter, LuaFlowNode calleeRead, LuaFlowNode calleeDerived, + LuaFlowNode sink + | + source.getModulePath() = fixture and + source.getValueRef() = "root.2@pc4:r0" and + outerResult.getModulePath() = fixture and + outerResult.getValueRef() = "root@pc15:r2" and + callerRead.getModulePath() = fixture and + callerRead.getValueRef() = "root@pc17:r2" and + callerDerived.getModulePath() = fixture and + callerDerived.getValueRef() = "root@pc17:r4" and + argument.getModulePath() = fixture and + argument.getValueRef() = "root@pc18:r4" and + parameter.getModulePath() = fixture and + parameter.getValueRef() = "root.3:r0" and + calleeRead.getModulePath() = fixture and + calleeRead.getValueRef() = "root.3@pc2:r0" and + calleeDerived.getModulePath() = fixture and + calleeDerived.getValueRef() = "root.3@pc2:r2" and + sink.getModulePath() = fixture and + sink.getValueRef() = "root.3@pc3:r2" and + flowNodeStep(source, outerResult, "return-to-result", _) and + flowNodeStep(outerResult, callerRead, "reaching-definition", _) and + flowNodeStep(callerRead, callerDerived, "same-instruction-dependence", _) and + flowNodeStep(callerDerived, argument, "reaching-definition", _) and + flowNodeStep(argument, parameter, "argument-to-parameter", _) and + flowNodeStep(parameter, calleeRead, "reaching-definition", _) and + flowNodeStep(calleeRead, calleeDerived, "same-instruction-dependence", _) and + flowNodeStep(calleeDerived, sink, "reaching-definition", _) and + genericFlowReachable(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef()) and + evidence = + source.toString() + " -> " + outerResult.toString() + " -> " + callerRead.toString() + " -> " + + callerDerived.toString() + " -> " + argument.toString() + " -> " + parameter.toString() + + " -> " + calleeRead.toString() + " -> " + calleeDerived.toString() + " -> " + + sink.toString() + ) + or + capability = "path.cross-module-complete" and + fixture = "cross-module-webcmd-popen/controller.luac" and + exists( + LuaFlowNode source, LuaFlowNode callerRead, LuaFlowNode callerDerived, LuaFlowNode argument, + LuaFlowNode parameter, LuaFlowNode calleeRead, LuaFlowNode calleeDerived, LuaFlowNode sink + | + source.getModulePath() = fixture and + source.getValueRef() = "root.1@pc4:r0" and + callerRead.getModulePath() = fixture and + callerRead.getValueRef() = "root.1@pc7:r0" and + callerDerived.getModulePath() = fixture and + callerDerived.getValueRef() = "root.1@pc7:r2" and + argument.getModulePath() = fixture and + argument.getValueRef() = "root.1@pc8:r2" and + parameter.getModulePath() = "cross-module-webcmd-popen/mtkwifi.luac" and + parameter.getValueRef() = "root.1:r0" and + calleeRead.getModulePath() = "cross-module-webcmd-popen/mtkwifi.luac" and + calleeRead.getValueRef() = "root.1@pc2:r0" and + calleeDerived.getModulePath() = "cross-module-webcmd-popen/mtkwifi.luac" and + calleeDerived.getValueRef() = "root.1@pc2:r2" and + sink.getModulePath() = "cross-module-webcmd-popen/mtkwifi.luac" and + sink.getValueRef() = "root.1@pc3:r2" and + flowNodeStep(source, callerRead, "reaching-definition", _) and + flowNodeStep(callerRead, callerDerived, "same-instruction-dependence", _) and + flowNodeStep(callerDerived, argument, "reaching-definition", _) and + flowNodeStep(argument, parameter, "argument-to-parameter", _) and + flowNodeStep(parameter, calleeRead, "reaching-definition", _) and + flowNodeStep(calleeRead, calleeDerived, "same-instruction-dependence", _) and + flowNodeStep(calleeDerived, sink, "reaching-definition", _) and + genericFlowReachable(source.getModulePath(), source.getValueRef(), sink.getModulePath(), + sink.getValueRef()) and + evidence = + source.toString() + " -> " + callerRead.toString() + " -> " + callerDerived.toString() + + " -> " + argument.toString() + " -> " + parameter.toString() + " -> " + + calleeRead.toString() + " -> " + calleeDerived.toString() + " -> " + sink.toString() + ) + or + capability = "interproc.arg-flow" and + exists( + string callsiteId, string fromArg, string targetModule, string targetPrototype, + string parameter, string provenance + | + interproceduralArgFlow(fixture, callsiteId, fromArg, targetModule, targetPrototype, parameter, + provenance) and + evidence = callsiteId + " " + fromArg + " -> " + targetModule + "::" + parameter + ) + or + capability = "interproc.return-flow" and + exists( + string callsiteId, string targetModule, string targetPrototype, string calleeReturn, + string callerResult, string provenance + | + interproceduralReturnFlow(fixture, callsiteId, targetModule, targetPrototype, calleeReturn, + callerResult, provenance) and + evidence = callsiteId + " " + targetModule + "::" + calleeReturn + " -> " + callerResult + ) + or + capability = "module.literal-require" and + fixture = "module-return-table-field-call/controller.luac" and + exists( + string modulePath, string prototypeId, int pc, string callsiteId, string requireString, + string argumentRef, string provenance + | + literalRequireCall(fixture, modulePath, prototypeId, pc, callsiteId, requireString, argumentRef, + provenance) and + evidence = callsiteId + " requires " + requireString + " via " + argumentRef + ) + or + capability = "module.typed-resolution" and + exists(LuaModuleResolution resolution | + fixture = resolution.getCallerModulePath() and + fixture = "cross-module-webcmd-popen/controller.luac" and + resolution.getStatus() = "matched" and + evidence = + resolution.getCallsiteId() + " " + resolution.getRequireString() + " -> " + + resolution.getTargetModulePath() + ) + or + capability = "module.require-resolution" and + exists( + string callsiteId, string requireString, string status, string fromModule, string targetModule, + string reason, string provenance + | + moduleResolution(fixture, callsiteId, requireString, status, fromModule, targetModule, reason, + provenance) and + status = "matched" and + evidence = fromModule + " requires " + requireString + " -> " + targetModule + ) + or + capability = "module.typed-export" and + exists(LuaModuleExport export | + fixture = export.getModulePath() and + export.getExportKind() = "returned-table-field" and + evidence = + export.getFieldName() + " " + export.getValueRef() + " -> " + export.getTargetPrototypeId() + ) + or + capability = "module.export-return-table" and + exists( + string modulePath, string exportKind, string fieldName, string valueRef, string targetPrototype, + string provenance + | + moduleExport(fixture, modulePath, exportKind, fieldName, valueRef, targetPrototype, provenance) and + exportKind = "returned-table-field" and + evidence = modulePath + " exports " + fieldName + " -> " + targetPrototype + ) + or + capability = "module.field-target" and + exists( + string fromModule, string callsiteId, string fieldName, string targetModule, + string targetPrototype, string provenance + | + moduleFieldCallTarget(fixture, fromModule, callsiteId, fieldName, targetModule, targetPrototype, + provenance) and + evidence = + callsiteId + " " + fieldName + " -> " + targetModule + "::" + targetPrototype + " via " + + provenance + ) + or + capability = "calltarget.cross-boundary" and + exists( + string callsiteId, string targetModule, string targetPrototype, string confidence, + string provenance + | + crossBoundaryCallTargetCandidate(fixture, callsiteId, targetModule, targetPrototype, confidence, + provenance) and + evidence = callsiteId + " -> " + targetModule + "::" + targetPrototype + ) +select fixture, capability, evidence diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.expected b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.expected @@ -0,0 +1 @@ + diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.ql b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.ql new file mode 100644 index 000000000000..d8c7e4e56ee3 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.ql @@ -0,0 +1,70 @@ +import codeql.lua.InterproceduralModuleTaint + +from string fixture, string forbidden, string evidence +where + fixture = "callsite-balanced-identity/input.luac" and + forbidden = "cross-callsite-return-contamination" and + genericFlowReachable(fixture, "root@pc5:r3", fixture, "root@pc8:r3") and + evidence = "root@pc5:r3 -> root@pc8:r3" + or + fixture = "bc-branch-negative/input.luac" and + forbidden = "branch-clean-value-produced-generic-path" and + genericFlowReachable(fixture, "root.0@pc1:r0", fixture, "root.1:r0") and + evidence = "root.0@pc1:r0 -> root.1:r0" + or + fixture = "unresolved-callee-negative/input.luac" and + forbidden = "unresolved-call-produced-generic-path" and + genericFlowReachable(fixture, "root.2@pc2:r3", fixture, "root.1:r0") and + evidence = "root.2@pc2:r3 -> root.1:r0" + or + fixture = "module-missing-field-negative/controller.luac" and + forbidden = "missing-field-produced-generic-cross-module-edge" and + genericFlowStep(fixture, "root.0@pc3:r1", "module-missing-field-negative/missingfn.luac", + "root.0:r0", _, _) and + evidence = "root.0@pc3:r1 -> missingfn.luac::root.0:r0" + or + fixture = "bc-taint-minimal-path/input.luac" and + forbidden = "direct-return-to-sink-parameter-shortcut" and + genericFlowStep(fixture, "root.0@pc1:r0", fixture, "root.1:r0", _, _) and + evidence = "root.0@pc1:r0 -> root.1:r0" + or + fixture = "bc-kill-overwrite/input.luac" and + forbidden = "killed-flow-produced-generic-path" and + genericFlowReachable(fixture, "root@pc3:r2", fixture, "root@pc7:r4") and + evidence = "root@pc3:r2 -> root@pc7:r4" + or + fixture = "unresolved-callee-negative/input.luac" and + forbidden = "unresolved-callee-produced-interproc-flow" and + exists( + string callsiteId, string fromArg, string targetModule, string targetPrototype, + string parameter, string provenance, string reason + | + interproceduralArgFlow(fixture, callsiteId, fromArg, targetModule, targetPrototype, parameter, + provenance) and + unresolvedCallBoundary(fixture, callsiteId, reason) and + evidence = callsiteId + ) + or + fixture = "module-missing-field-negative/controller.luac" and + forbidden = "missing-field-produced-calltarget" and + exists( + string callsiteId, string targetModule, string targetPrototype, string confidence, + string provenance + | + crossBoundaryCallTargetCandidate(fixture, callsiteId, targetModule, targetPrototype, confidence, + provenance) and + evidence = callsiteId + ) + or + fixture = "ambiguous-unresolved-dynamic-module-negative/controller.luac" and + forbidden = "ambiguous-or-dynamic-require-produced-linkage" and + exists( + string callsiteId, string requireString, string status, string fromModule, string targetModule, + string reason, string provenance + | + moduleResolution(fixture, callsiteId, requireString, status, fromModule, targetModule, reason, + provenance) and + status = "matched" and + evidence = callsiteId + ) +select fixture, forbidden, evidence diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/SAMPLE-MANIFEST.md b/lua/ql/test/library-tests/interprocedural-module-taint/SAMPLE-MANIFEST.md new file mode 100644 index 000000000000..59639f34ae1f --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/SAMPLE-MANIFEST.md @@ -0,0 +1,27 @@ +# Interprocedural module taint sample manifest + +These committed Lua 5.1 bytecode fixtures exercise interprocedural and module +semantics without external test data. + +| Fixture | Expected boundary | +| --- | --- | +| `same-module-formvalue-execute/input.luac` | Resolved same-artifact argument and return flow. | +| `cross-module-webcmd-popen/{controller,mtkwifi}.luac` | Literal require, module export, cross-module call target, and taint path. | +| `module-return-table-field-call/{controller,library}.luac` | Returned-table field call target. | +| `bc-taint-minimal-path/input.luac` | Minimal same-artifact taint path. | +| `unresolved-callee-negative/input.luac` | Unresolved callee does not create interprocedural flow. | +| `ambiguous-unresolved-dynamic-module-negative/*.luac` | Missing, ambiguous, and dynamic requires remain explicit boundaries. | +| `module-missing-field-negative/{controller,library}.luac` | A missing export field does not create a target. | +| `bc-kill-overwrite/input.luac` | Overwritten values do not reach the sink. | +| `bc-branch-negative/input.luac` | No unproved branch flow is synthesized. | + +Reviewer command: + +```bash +CODEQL=/absolute/path/to/codeql +"$CODEQL" test run lua/ql/test/library-tests/interprocedural-module-taint \ + --search-path .:lua --threads=0 --verbosity=progress +``` + +The `.expected` files in this directory are the complete oracle for this +focused test set. diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.lua.txt new file mode 100644 index 000000000000..3b4da7ff74ef --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.lua.txt @@ -0,0 +1,7 @@ +local choice = ... +local function run() + local mod = require(choice) + return mod.exec("whoami") +end + +return run diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.luac b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.luac new file mode 100644 index 000000000000..3e94967def52 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.lua.txt new file mode 100644 index 000000000000..93baf44ebfe4 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.lua.txt @@ -0,0 +1,5 @@ +local M = {} +function M.exec(cmd) + return io.popen(cmd) +end +return M diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.luac b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.luac new file mode 100644 index 000000000000..2b522342a60c Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.lua.txt new file mode 100644 index 000000000000..dd8c8f8c785b --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.lua.txt @@ -0,0 +1,6 @@ +local function run() + local mod = require("missing.module") + return mod.exec("whoami") +end + +return run diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.luac b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.luac new file mode 100644 index 000000000000..e988dfcc5df7 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.lua.txt new file mode 100644 index 000000000000..93baf44ebfe4 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.lua.txt @@ -0,0 +1,5 @@ +local M = {} +function M.exec(cmd) + return io.popen(cmd) +end +return M diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.luac b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.luac new file mode 100644 index 000000000000..5213e606ee5d Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/bc-branch-negative/input.luac b/lua/ql/test/library-tests/interprocedural-module-taint/bc-branch-negative/input.luac new file mode 100644 index 000000000000..29c4dfe7ad77 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/bc-branch-negative/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/bc-kill-overwrite/input.luac b/lua/ql/test/library-tests/interprocedural-module-taint/bc-kill-overwrite/input.luac new file mode 100644 index 000000000000..7ecd2a1f96b9 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/bc-kill-overwrite/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/bc-taint-minimal-path/input.luac b/lua/ql/test/library-tests/interprocedural-module-taint/bc-taint-minimal-path/input.luac new file mode 100644 index 000000000000..71021c22082e Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/bc-taint-minimal-path/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/input.luac b/lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/input.luac new file mode 100644 index 000000000000..b0daecbb3aa2 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/source.lua b/lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/source.lua new file mode 100644 index 000000000000..a2d18854651c --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/source.lua @@ -0,0 +1,10 @@ +local function identity(value) + local result = value + return result +end + +local tainted = input() +local tainted_result = identity(tainted) +local clean_result = identity("clean") + +return tainted_result, clean_result diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.lua.txt new file mode 100644 index 000000000000..ae878dbbea4d --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.lua.txt @@ -0,0 +1,14 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +local mtkwifi = require("mtkwifi") + +function webcmd() + local command_text = luci.http.formvalue("cmd") + return mtkwifi.func_unknow_0_12(command_text) +end + +webcmd() diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.luac b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.luac new file mode 100644 index 000000000000..494e02b445a4 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.lua.txt new file mode 100644 index 000000000000..f18bd010f4ae --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.lua.txt @@ -0,0 +1,13 @@ +local M = {} + +io = { + popen = function(cmd) + return cmd + end +} + +function M.func_unknow_0_12(command_text) + return io.popen(command_text) +end + +return M diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.luac b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.luac new file mode 100644 index 000000000000..7fa65fafbfd5 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.lua.txt new file mode 100644 index 000000000000..3c7bcd3aedd8 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.lua.txt @@ -0,0 +1,6 @@ +local lib = require("missingfn") +local function run() + return lib.missing("whoami") +end + +return run diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.luac b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.luac new file mode 100644 index 000000000000..484533ab2829 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.lua.txt new file mode 100644 index 000000000000..73645ab1acec --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.lua.txt @@ -0,0 +1,5 @@ +local M = {} +function M.run(cmd) + return io.popen(cmd) +end +return M diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.luac b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.luac new file mode 100644 index 000000000000..1422802a13e1 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.lua.txt new file mode 100644 index 000000000000..c92a42171507 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.lua.txt @@ -0,0 +1,15 @@ +local function via_local() + local lib = require("samplelib") + local cmd = luci.http.formvalue("cmd") + return lib.run(cmd) +end + +local function via_direct() + local cmd = luci.http.formvalue("cmd") + return require("samplelib").run(cmd) +end + +return { + via_local = via_local, + via_direct = via_direct, +} diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.luac b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.luac new file mode 100644 index 000000000000..13e600068c41 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.lua.txt new file mode 100644 index 000000000000..1fe13e23c9eb --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.lua.txt @@ -0,0 +1,7 @@ +local M = {} + +function M.run(cmd) + return os.execute(cmd) +end + +return M diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.luac b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.luac new file mode 100644 index 000000000000..92ba7f389fa7 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/input.luac b/lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/input.luac new file mode 100644 index 000000000000..6a0164daf1e9 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/source.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/source.lua.txt new file mode 100644 index 000000000000..cb1d785e7926 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/source.lua.txt @@ -0,0 +1,22 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = { + execute = function(cmd) + return cmd + end +} + +local function read_command() + return luci.http.formvalue("cmd") +end + +local function run_command(command_text) + return os.execute(command_text) +end + +local command_text = read_command() +run_command(command_text) diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/input.luac b/lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/input.luac new file mode 100644 index 000000000000..63710c6f7225 Binary files /dev/null and b/lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/input.luac differ diff --git a/lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/source.lua.txt b/lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/source.lua.txt new file mode 100644 index 000000000000..f91cdbcf6c88 --- /dev/null +++ b/lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/source.lua.txt @@ -0,0 +1,15 @@ +local source = function() + return luci.http.formvalue("cmd") +end + +local sink = function(value) + return os.execute(value) +end + +local dispatch = function(fn, value) + return fn(value) +end + +local unresolved = source() +dispatch(target, unresolved) +sink("clean") diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.expected b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.expected @@ -0,0 +1 @@ + diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.ql b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.ql new file mode 100644 index 000000000000..d215250dfe62 --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.ql @@ -0,0 +1,91 @@ +import codeql.lua.IntraproceduralSemantics +import codeql.lua.InterproceduralModuleTaint + +from string fixture, string subject, string detail +where + exists(LuaTableFieldFlow flow | + flow.getModulePath() = "bc-table-global-upvalue/input.luac" and + flow.getProvenance() = "bytecode-only" + ) and + fixture = "bc-table-global-upvalue" and + subject = "forbidden legacy table flow producer" and + detail = "bytecode-only" + or + exists(LuaGlobalFlow flow | + flow.getFixtureId() = "bc-table-global-upvalue/input.luac" and + flow.getGlobalName() = "analysis_global" and + flow.getProvenance() = "bytecode-only" + ) and + fixture = "bc-table-global-upvalue" and + subject = "forbidden legacy global flow producer" and + detail = "unsupported _G mutation" + or + exists(LuaLocalFlow flow | + flow.getModulePath() = "bc-kill-overwrite/input.luac" and + flow.getEdgeKind() = "may-reaching-definition" + ) and + fixture = "bc-kill-overwrite" and + subject = "forbidden legacy local flow producer" and + detail = "may-reaching-definition" + or + localFlowReachable("bc-kill-overwrite/input.luac", "root@pc3:r2", "root@pc7:r4") and + fixture = "bc-kill-overwrite" and + subject = "forbidden killed flow" and + detail = "root@pc3:r2 -> root@pc7:r4" + or + localFlowReachable("defuse-unrelated-register-negative/input.luac", "root@pc4:r2", "root@pc9:r6") and + fixture = "defuse-unrelated-register-negative" and + subject = "forbidden cross prototype register reuse" and + detail = "root@pc4:r2 -> root@pc9:r6" + or + tableFieldFlow("table-dynamic-key-negative/input.luac", _, _, "root@pc3:r2", "root@pc4:r3") and + fixture = "table-dynamic-key-negative" and + subject = "forbidden dynamic key table flow" and + detail = "root@pc3:r2 -> root@pc4:r3" + or + tableFieldFlow("table-dynamic-key-negative/input.luac", _, _, "root@pc3:r2", "root@pc5:r4") and + fixture = "table-dynamic-key-negative" and + subject = "forbidden missing field table flow" and + detail = "root@pc3:r2 -> root@pc5:r4" + or + tableFieldFlow("call-result-table-flow/input.luac", _, _, "root.1@pc2:r0", "root.1@pc3:r3") and + fixture = "call-result-table-flow" and + subject = "forbidden dynamic call-result field flow" and + detail = "root.1@pc2:r0 -> root.1@pc3:r3" + or + globalFlowStep("global-dynamic-environment-negative/input.luac", _, _, "root@pc4:r2", + "root@pc3:r1") and + fixture = "global-dynamic-environment-negative" and + subject = "forbidden dynamic env global flow" and + detail = "root@pc3:r1 -> root@pc4:r2" + or + globalFlowStep("global-dynamic-environment-negative/input.luac", _, _, "root@pc5:r3", + "root@pc3:r1") and + fixture = "global-dynamic-environment-negative" and + subject = "forbidden missing global flow" and + detail = "root@pc3:r1 -> root@pc5:r3" + or + upvalueFlowStep("upvalue-mutation-negative/input.luac", _, "root.0@pc0:r0", "root.0@pc3:r1", _) and + fixture = "upvalue-mutation-negative" and + subject = "forbidden stale upvalue reuse" and + detail = "root.0@pc0:r0 -> root.0@pc3:r1" + or + exists(LuaUpvalueFlow flow | + flow.getFixtureId() = "bc-table-global-upvalue/input.luac" and + flow.getUpvalueId() = "root.0:u0" and + flow.getCaptureRef() = "root.0:u0" and + flow.getProvenance() = "bytecode-only" + ) and + fixture = "bc-table-global-upvalue" and + subject = "forbidden legacy upvalue flow producer" and + detail = "synthetic capture/read/write" + or + exists(LuaCallResolution resolution | + resolution.getCallerModulePath() = "bc-call-candidate-unresolved/input.luac" and + resolution.getCallsiteId() = "root.1@pc2" and + resolution.getResolvedName() = "source-function-name" + ) and + fixture = "bc-call-candidate-unresolved" and + subject = "forbidden guessed source target" and + detail = "root.1@pc2 -> source-function-name" +select fixture, subject, detail diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.expected b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.expected new file mode 100644 index 000000000000..401b67e2a64e --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.expected @@ -0,0 +1,16 @@ +| bc-call-candidate-unresolved | calltarget.boundary | root.1@pc2 | param-derived | unresolved-call-target | +| bc-call-candidate-unresolved | calltarget.resolution | root@pc5 | root.1 | closure-move | +| bc-kill-overwrite | rda.analyzer-relation | root@pc4:r2 -> root@pc6:r2 | reaching-definition | bytecode-only,cfg-rda | +| bc-kill-overwrite | rda.kill-gen | bc-kill-overwrite:clean-constant-to-sink | root@pc4:r2 -> root@pc7:r4 | reachable | +| bc-table-global-upvalue | table.analyzer-relation | root.0@pc0:r1.key | root.0@pc1:r0 -> root.0@pc3:r3 | bytecode-only,precise-table-field | +| bc-table-global-upvalue | table.field-flow | root.0@pc1:r0 -> root.0@pc6:r3 | root.0@pc0:r1/key | table/global/upvalue carrier | +| bc-table-global-upvalue | table.generic-field-flow | root.0@pc0:r1.key | root.0@pc1:r0 -> root.0@pc3:r3 | bytecode-only,precise-table-field | +| bc-table-global-upvalue | table.object-carrier | root.0@pc0:r1 | root.0@pc1:r0 -> root.0@pc3:r1 | bytecode-only,mutable-table-object | +| bc-table-global-upvalue | upvalue.capture-read-write | root.0:u0 | root@pc2:r0 -> root.0@pc4:r4 | bytecode-only,derived-upvalue-flow | +| call-result-table-flow | table.call-result-object-flow | dynamic-write-to-later-read | root.1@pc2:r0 -> root.1@pc3:r3 | bytecode-only,mutable-table-object | +| defuse-transitive-chain | analysis.boundary | root@pc9 | open-return-tail | only predecessor-proven return slots are modeled | +| defuse-transitive-chain | defuse.transitive-chain | defuse-transitive-chain:register-chain | root@pc3:r2 -> root@pc8:r6 | reachable | +| global-state-write-read | global.write-read | shared_global | root.0@pc0:r0 -> root.0@pc1:r1 | bytecode-only,precise-global-state | +| table-dynamic-key-negative | table.whole-object-flow | dynamic-key-read | root@pc2:r2 -> root@pc4:r3 | same-proven-table | +| table-dynamic-key-negative | table.whole-object-flow | missing-key-read | root@pc2:r2 -> root@pc5:r4 | same-proven-table | +| upvalue-mutation-negative | upvalue.mutation-evidence | root.0:u0 | root.0@pc2:r1 -> root.0@pc3:r1 | post-write-evidence | diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.ql b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.ql new file mode 100644 index 000000000000..7db2854190e8 --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.ql @@ -0,0 +1,198 @@ +import codeql.lua.IntraproceduralSemantics +import codeql.lua.InterproceduralModuleTaint + +from string fixture, string capability, string subject, string detail, string value +where + localFlowReachable("bc-kill-overwrite/input.luac", "root@pc4:r2", "root@pc7:r4") and + fixture = "bc-kill-overwrite" and + capability = "rda.kill-gen" and + subject = "bc-kill-overwrite:clean-constant-to-sink" and + detail = "root@pc4:r2 -> root@pc7:r4" and + value = "reachable" + or + exists(LuaLocalFlow flow | + flow.getModulePath() = "bc-kill-overwrite/input.luac" and + flow.getPrototypeId() = "root" and + flow.getSourceRef() = "root@pc4:r2" and + flow.getSinkRef() = "root@pc6:r2" and + flow.getEdgeKind() = "reaching-definition" and + flow.getProvenance() = "bytecode-only,cfg-rda" + ) and + fixture = "bc-kill-overwrite" and + capability = "rda.analyzer-relation" and + subject = "root@pc4:r2 -> root@pc6:r2" and + detail = "reaching-definition" and + value = "bytecode-only,cfg-rda" + or + localFlowReachable("defuse-transitive-chain/input.luac", "root@pc3:r2", "root@pc8:r6") and + fixture = "defuse-transitive-chain" and + capability = "defuse.transitive-chain" and + subject = "defuse-transitive-chain:register-chain" and + detail = "root@pc3:r2 -> root@pc8:r6" and + value = "reachable" + or + exists(LuaAnalysisBoundary boundary | + boundary.getModulePath() = "defuse-transitive-chain/input.luac" and + boundary.getPrototypeId() = "root" and + boundary.getSiteId() = "root@pc9" and + boundary.getBoundaryKind() = "open-return-tail" and + boundary.getReason() = "only predecessor-proven return slots are modeled" and + boundary.getProvenance() = "bytecode-only,open-slot-boundary" + ) and + fixture = "defuse-transitive-chain" and + capability = "analysis.boundary" and + subject = "root@pc9" and + detail = "open-return-tail" and + value = "only predecessor-proven return slots are modeled" + or + tableFieldFlow("bc-table-global-upvalue/input.luac", "root.0@pc0:r1", "key", "root.0@pc1:r0", + "root.0@pc3:r3") and + localFlowReachable("bc-table-global-upvalue/input.luac", "root.0@pc3:r3", "root.0@pc6:r3") and + fixture = "bc-table-global-upvalue" and + capability = "table.field-flow" and + subject = "root.0@pc1:r0 -> root.0@pc6:r3" and + detail = "root.0@pc0:r1/key" and + value = "table/global/upvalue carrier" + or + exists(LuaTableFieldFlow flow | + flow.getModulePath() = "bc-table-global-upvalue/input.luac" and + flow.getPrototypeId() = "root.0" and + flow.getTableRef() = "root.0@pc0:r1" and + flow.getFieldName() = "key" and + flow.getWriteRef() = "root.0@pc1:r0" and + flow.getReadRef() = "root.0@pc3:r3" and + flow.getProvenance() = "bytecode-only,precise-table-field" + ) and + fixture = "bc-table-global-upvalue" and + capability = "table.analyzer-relation" and + subject = "root.0@pc0:r1.key" and + detail = "root.0@pc1:r0 -> root.0@pc3:r3" and + value = "bytecode-only,precise-table-field" + or + genericFlowStep("bc-table-global-upvalue/input.luac", "root.0@pc1:r0", + "bc-table-global-upvalue/input.luac", "root.0@pc3:r3", "table-field", + "bytecode-only,precise-table-field") and + genericFlowReachable("bc-table-global-upvalue/input.luac", "root.0@pc1:r0", + "bc-table-global-upvalue/input.luac", "root.0@pc3:r3") and + fixture = "bc-table-global-upvalue" and + capability = "table.generic-field-flow" and + subject = "root.0@pc0:r1.key" and + detail = "root.0@pc1:r0 -> root.0@pc3:r3" and + value = "bytecode-only,precise-table-field" + or + exists(LuaLocalFlow flow | + flow.getModulePath() = "bc-table-global-upvalue/input.luac" and + flow.getPrototypeId() = "root.0" and + flow.getSourceRef() = "root.0@pc1:r0" and + flow.getSinkRef() = "root.0@pc3:r1" and + flow.getEdgeKind() = "table-object-dependence" and + flow.getProvenance() = "bytecode-only,mutable-table-object" + ) and + fixture = "bc-table-global-upvalue" and + capability = "table.object-carrier" and + subject = "root.0@pc0:r1" and + detail = "root.0@pc1:r0 -> root.0@pc3:r1" and + value = "bytecode-only,mutable-table-object" + or + genericFlowReachable("table-dynamic-key-negative/input.luac", "root@pc2:r2", + "table-dynamic-key-negative/input.luac", "root@pc4:r3") and + fixture = "table-dynamic-key-negative" and + capability = "table.whole-object-flow" and + subject = "dynamic-key-read" and + detail = "root@pc2:r2 -> root@pc4:r3" and + value = "same-proven-table" + or + genericFlowReachable("table-dynamic-key-negative/input.luac", "root@pc2:r2", + "table-dynamic-key-negative/input.luac", "root@pc5:r4") and + fixture = "table-dynamic-key-negative" and + capability = "table.whole-object-flow" and + subject = "missing-key-read" and + detail = "root@pc2:r2 -> root@pc5:r4" and + value = "same-proven-table" + or + exists(LuaLocalFlow flow | + flow.getModulePath() = "call-result-table-flow/input.luac" and + flow.getPrototypeId() = "root.1" and + flow.getSourceRef() = "root.1@pc2:r0" and + flow.getSinkRef() = "root.1@pc3:r2" and + flow.getEdgeKind() = "table-object-dependence" and + flow.getProvenance() = "bytecode-only,mutable-table-object" + ) and + genericFlowReachable("call-result-table-flow/input.luac", "root.1@pc2:r0", + "call-result-table-flow/input.luac", "root.1@pc3:r3") and + fixture = "call-result-table-flow" and + capability = "table.call-result-object-flow" and + subject = "dynamic-write-to-later-read" and + detail = "root.1@pc2:r0 -> root.1@pc3:r3" and + value = "bytecode-only,mutable-table-object" + or + exists(LuaGlobalFlow flow | + flow.getFixtureId() = "global-state-write-read/input.luac" and + flow.getGlobalName() = "shared_global" and + flow.getWriteRef() = "root.0@pc0:r0" and + flow.getReadRef() = "root.0@pc1:r1" and + flow.getValueRef() = "root.0@pc0:r0" and + flow.getProvenance() = "bytecode-only,precise-global-state" + ) and + globalFlowStep("global-state-write-read/input.luac", "shared_global", "root.0@pc0:r0", + "root.0@pc1:r1", "root.0@pc0:r0") and + fixture = "global-state-write-read" and + capability = "global.write-read" and + subject = "shared_global" and + detail = "root.0@pc0:r0 -> root.0@pc1:r1" and + value = "bytecode-only,precise-global-state" + or + exists(LuaUpvalueFlow flow | + flow.getFixtureId() = "bc-table-global-upvalue/input.luac" and + flow.getUpvalueId() = "root.0:u0" and + flow.getCaptureRef() = "root@pc2:r0" and + flow.getReadRef() = "root.0@pc4:r4" and + flow.getWriteRef() = "" and + flow.getProvenance() = "bytecode-only,derived-upvalue-flow" + ) and + localFlowReachable("bc-table-global-upvalue/input.luac", "root.0@pc4:r4", "root.0@pc6:r3") and + fixture = "bc-table-global-upvalue" and + capability = "upvalue.capture-read-write" and + subject = "root.0:u0" and + detail = "root@pc2:r0 -> root.0@pc4:r4" and + value = "bytecode-only,derived-upvalue-flow" + or + upvalueFlowStep("upvalue-mutation-negative/input.luac", "root.0:u0", "root@pc2:r0", + "root.0@pc3:r1", "root.0@pc2:r1") and + fixture = "upvalue-mutation-negative" and + capability = "upvalue.mutation-evidence" and + subject = "root.0:u0" and + detail = "root.0@pc2:r1 -> root.0@pc3:r1" and + value = "post-write-evidence" + or + exists(LuaCallResolution resolution | + resolution.getCallerModulePath() = "bc-call-candidate-unresolved/input.luac" and + resolution.getCallerPrototypeId() = "root" and + resolution.getCallsiteId() = "root@pc5" and + resolution.getTargetValueRef() = "root@pc5:r2" and + resolution.getResolvedName() = "invoke" and + resolution.getResolutionKind() = "closure-move" and + resolution.getTargetModulePath() = "bc-call-candidate-unresolved/input.luac" and + resolution.getTargetPrototypeId() = "root.1" and + resolution.getProvenance() = "bytecode-only,closure-move-target" + ) and + fixture = "bc-call-candidate-unresolved" and + capability = "calltarget.resolution" and + subject = "root@pc5" and + detail = "root.1" and + value = "closure-move" + or + exists(LuaAnalysisBoundary boundary | + boundary.getModulePath() = "bc-call-candidate-unresolved/input.luac" and + boundary.getPrototypeId() = "root.1" and + boundary.getSiteId() = "root.1@pc2" and + boundary.getBoundaryKind() = "unresolved-call-target" and + boundary.getReason() = "param-derived" and + boundary.getProvenance() = "bytecode-only,call-resolution-boundary" + ) and + fixture = "bc-call-candidate-unresolved" and + capability = "calltarget.boundary" and + subject = "root.1@pc2" and + detail = "param-derived" and + value = "unresolved-call-target" +select fixture, capability, subject, detail, value diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac new file mode 100644 index 000000000000..9672d77f6b77 Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/bc-kill-overwrite/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/bc-kill-overwrite/input.luac new file mode 100644 index 000000000000..7ecd2a1f96b9 Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/bc-kill-overwrite/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/input.luac new file mode 100644 index 000000000000..36ca0e3996ef Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/source.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/source.lua.txt new file mode 100644 index 000000000000..27900eba2fbc --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/source.lua.txt @@ -0,0 +1,9 @@ +local seed = "up" + +local function build(value) + local t = { key = value } + _G.analysis_global = t.key .. seed + return _G.analysis_global +end + +return build("table") diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/input.luac new file mode 100644 index 000000000000..18ca35f7edca Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/source.lua b/lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/source.lua new file mode 100644 index 000000000000..9ff8278eef07 --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/source.lua @@ -0,0 +1,11 @@ +local function make_table() + return {} +end + +local function read_after_write(value, key) + local state = make_table() + state[key] = value + return state.result +end + +return read_after_write diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/input.luac new file mode 100644 index 000000000000..03307cadb194 Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/source.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/source.lua.txt new file mode 100644 index 000000000000..e5e71decc358 --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/source.lua.txt @@ -0,0 +1,12 @@ +local function source() + return "tainted" +end + +local function sink(x) + return x +end + +local a = source() +local b = a +local c = b +return sink(c) diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/input.luac new file mode 100644 index 000000000000..b75dc7cf2ef6 Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/source.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/source.lua.txt new file mode 100644 index 000000000000..3f26b229339f --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/source.lua.txt @@ -0,0 +1,16 @@ +local function source() + return "tainted" +end + +local function sink(x) + return x +end + +local function unrelated() + local r = "clean" + return r +end + +local a = source() +local b = unrelated() +return sink(b) diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/input.luac new file mode 100644 index 000000000000..c6a5cd40efac Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/source.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/source.lua.txt new file mode 100644 index 000000000000..35f5fa59087b --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/source.lua.txt @@ -0,0 +1,9 @@ +local key = "global_name" +local value = "payload" + +_G[key] = value + +local chosen = global_name +local other = missing_name + +return chosen, other diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.lua.txt new file mode 100644 index 000000000000..5c375d38bb2e --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.lua.txt @@ -0,0 +1,6 @@ +local function precise_global(value) + shared_global = value + return shared_global +end + +return precise_global diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.luac new file mode 100644 index 000000000000..e1b485b1492d Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/input.luac new file mode 100644 index 000000000000..7d26549a70f8 Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/source.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/source.lua.txt new file mode 100644 index 000000000000..0367b5b28417 --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/source.lua.txt @@ -0,0 +1,10 @@ +local tbl = {} +local dyn = "runtime" +local value = "payload" + +tbl[dyn] = value + +local chosen = tbl["fixed"] +local missing = tbl["missing"] + +return chosen, missing diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/input.luac b/lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/input.luac new file mode 100644 index 000000000000..a759c66168c6 Binary files /dev/null and b/lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/input.luac differ diff --git a/lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/source.lua.txt b/lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/source.lua.txt new file mode 100644 index 000000000000..df57e36e2457 --- /dev/null +++ b/lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/source.lua.txt @@ -0,0 +1,10 @@ +local seed = "old" + +local function mutate() + local before = seed + seed = "new" + local after = seed + return before, after +end + +return mutate() diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.expected b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.expected new file mode 100644 index 000000000000..135aaed86a60 --- /dev/null +++ b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.expected @@ -0,0 +1,4 @@ +| midstream-guard.active-suppression | guarded path has no active report | +| midstream-guard.classification | middle-module guard sanitizer suppresses the downstream sink | +| midstream-guard.unrelated-active | unrelated sanitizer argument does not suppress the source | +| midstream-guard.unrelated-not-sanitized | unrelated sanitizer argument is not on the dataflow chain | diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.ql b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.ql new file mode 100644 index 000000000000..c6c10f5014cc --- /dev/null +++ b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.ql @@ -0,0 +1,38 @@ +import codeql.lua.RulesSanitizerReport + +from string capability, string evidence +where + capability = "midstream-guard.classification" and + reportClassification("source.luac", "root@pc8:r2", "neutral/sink.luac", "root.0@pc3:r2", + "sanitized", "sanitized path suppressed") and + evidence = "middle-module guard sanitizer suppresses the downstream sink" + or + capability = "midstream-guard.active-suppression" and + not exists( + LuaFlowNode source, LuaFlowNode sink, string classification, string reason, string provenance + | + source.getModulePath() = "source.luac" and + source.getValueRef() = "root@pc8:r2" and + sink.getModulePath() = "neutral/sink.luac" and + sink.getValueRef() = "root.0@pc3:r2" and + activeReportPath(source, sink, classification, reason, provenance) + ) and + evidence = "guarded path has no active report" + or + capability = "midstream-guard.unrelated-active" and + exists( + LuaFlowNode source, LuaFlowNode sink, string classification, string reason, string provenance + | + source.getModulePath() = "source.luac" and + source.getValueRef() = "root@pc14:r3" and + sink.getModulePath() = "neutral/sink.luac" and + sink.getValueRef() = "root.0@pc3:r2" and + activeReportPath(source, sink, classification, reason, provenance) + ) and + evidence = "unrelated sanitizer argument does not suppress the source" + or + capability = "midstream-guard.unrelated-not-sanitized" and + not reportClassification("source.luac", "root@pc14:r3", "neutral/sink.luac", "root.0@pc3:r2", + "sanitized", "sanitized path suppressed") and + evidence = "unrelated sanitizer argument is not on the dataflow chain" +select capability, evidence diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.lua b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.lua new file mode 100644 index 000000000000..a59ecc44ed1f --- /dev/null +++ b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.lua @@ -0,0 +1,16 @@ +local sink = require("neutral.sink") +local M = {} + +function M.forward(value) + if tonumber(value) then + sink.consume(value) + end +end + +function M.forward_unrelated(value) + if tonumber("constant") then + sink.consume(value) + end +end + +return M diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.luac b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.luac new file mode 100644 index 000000000000..ae966b7a8247 Binary files /dev/null and b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.luac differ diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.lua b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.lua new file mode 100644 index 000000000000..94093537af81 --- /dev/null +++ b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.consume(value) + os.execute(value) +end + +return M diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.luac b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.luac new file mode 100644 index 000000000000..ea4dcb19b237 Binary files /dev/null and b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.luac differ diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.lua b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.lua new file mode 100644 index 000000000000..9ab2dd408123 --- /dev/null +++ b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.lua @@ -0,0 +1,8 @@ +local http = require("neutral.http") +local middle = require("neutral.middle") + +local guarded = http.formvalue("guarded") +middle.forward(guarded) + +local unguarded = http.formvalue("unguarded") +middle.forward_unrelated(unguarded) diff --git a/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.luac b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.luac new file mode 100644 index 000000000000..f3a7700e3b4c Binary files /dev/null and b/lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/PathAlternatives.expected b/lua/ql/test/library-tests/path-alternatives/PathAlternatives.expected new file mode 100644 index 000000000000..a0d4e9256a00 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/PathAlternatives.expected @@ -0,0 +1,8 @@ +| active-only | true-positive | +| guard-overwritten | sanitized | +| merged | sanitized | +| mixed | sanitized | +| mixed | true-positive | +| same-route-mixed | true-positive | +| sanitizer-only | sanitized | +| sibling-return-active-only | true-positive | diff --git a/lua/ql/test/library-tests/path-alternatives/PathAlternatives.ql b/lua/ql/test/library-tests/path-alternatives/PathAlternatives.ql new file mode 100644 index 000000000000..bc19efb3d903 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/PathAlternatives.ql @@ -0,0 +1,39 @@ +import codeql.lua.RulesSanitizerReport + +private predicate scenarioModules(string scenario, string sourceModule, string sinkModule) { + scenario = "active-only" and + sourceModule = "active_controller.luac" and + sinkModule = "active_sink.luac" + or + scenario = "mixed" and + sourceModule = "mixed_controller.luac" and + sinkModule = "mixed_sink.luac" + or + scenario = "same-route-mixed" and + sourceModule = "same_route_controller.luac" and + sinkModule = "same_route_sink.luac" + or + scenario = "sibling-return-active-only" and + sourceModule = "sibling_return_controller.luac" and + sinkModule = "sibling_return_sink.luac" + or + scenario = "merged" and + sourceModule = "merged_controller.luac" and + sinkModule = "merged_sink.luac" + or + scenario = "guard-overwritten" and + sourceModule = "overwrite_controller.luac" and + sinkModule = "overwrite_sink.luac" + or + scenario = "sanitizer-only" and + sourceModule = "sanitized_controller.luac" and + sinkModule = "sanitized_sink.luac" +} + +from string scenario, string classification +where + exists(string sourceModule, string sourceRef, string sinkModule, string sinkRef, string reason | + scenarioModules(scenario, sourceModule, sinkModule) and + reportClassification(sourceModule, sourceRef, sinkModule, sinkRef, classification, reason) + ) +select scenario, classification diff --git a/lua/ql/test/library-tests/path-alternatives/active_controller.lua b/lua/ql/test/library-tests/path-alternatives/active_controller.lua new file mode 100644 index 000000000000..25ddd978150a --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/active_controller.lua @@ -0,0 +1,8 @@ +local sink = require("active_sink") + +function source() + return "input" +end + +local tainted = source() +sink.send(tainted) diff --git a/lua/ql/test/library-tests/path-alternatives/active_controller.luac b/lua/ql/test/library-tests/path-alternatives/active_controller.luac new file mode 100644 index 000000000000..fbad0f44d4ae Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/active_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/active_sink.lua b/lua/ql/test/library-tests/path-alternatives/active_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/active_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/active_sink.luac b/lua/ql/test/library-tests/path-alternatives/active_sink.luac new file mode 100644 index 000000000000..6192b009a4cc Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/active_sink.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/merged_controller.lua b/lua/ql/test/library-tests/path-alternatives/merged_controller.lua new file mode 100644 index 000000000000..2ac794567f7c --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/merged_controller.lua @@ -0,0 +1,7 @@ +local middle = require("merged_middle") + +function source() + return "input" +end + +middle.send(source()) diff --git a/lua/ql/test/library-tests/path-alternatives/merged_controller.luac b/lua/ql/test/library-tests/path-alternatives/merged_controller.luac new file mode 100644 index 000000000000..34071c05be4f Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/merged_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/merged_middle.lua b/lua/ql/test/library-tests/path-alternatives/merged_middle.lua new file mode 100644 index 000000000000..63765bdfb697 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/merged_middle.lua @@ -0,0 +1,19 @@ +local M = {} +local sink = require("merged_sink") + +function shellquote(value) + return value +end + +function M.send(value) + local cleaned = shellquote(value) + local forwarded + if condition then + forwarded = cleaned + else + forwarded = value + end + sink.send(forwarded) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/merged_middle.luac b/lua/ql/test/library-tests/path-alternatives/merged_middle.luac new file mode 100644 index 000000000000..5dc08efe2c19 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/merged_middle.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/merged_sink.lua b/lua/ql/test/library-tests/path-alternatives/merged_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/merged_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/merged_sink.luac b/lua/ql/test/library-tests/path-alternatives/merged_sink.luac new file mode 100644 index 000000000000..6192b009a4cc Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/merged_sink.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/mixed_cleaner.lua b/lua/ql/test/library-tests/path-alternatives/mixed_cleaner.lua new file mode 100644 index 000000000000..76b8525b3e02 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/mixed_cleaner.lua @@ -0,0 +1,13 @@ +local M = {} +local sink = require("mixed_sink") + +function shellquote(value) + return value +end + +function M.send(value) + local cleaned = shellquote(value) + sink.send(cleaned) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/mixed_cleaner.luac b/lua/ql/test/library-tests/path-alternatives/mixed_cleaner.luac new file mode 100644 index 000000000000..c20c3daf3f93 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/mixed_cleaner.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/mixed_controller.lua b/lua/ql/test/library-tests/path-alternatives/mixed_controller.lua new file mode 100644 index 000000000000..b8ae7b41e26c --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/mixed_controller.lua @@ -0,0 +1,13 @@ +local cleaner = require("mixed_cleaner") +local sink = require("mixed_sink") + +function source() + return "input" +end + +local tainted = source() +if condition then + cleaner.send(tainted) +else + sink.send(tainted) +end diff --git a/lua/ql/test/library-tests/path-alternatives/mixed_controller.luac b/lua/ql/test/library-tests/path-alternatives/mixed_controller.luac new file mode 100644 index 000000000000..62994b1090ed Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/mixed_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/mixed_sink.lua b/lua/ql/test/library-tests/path-alternatives/mixed_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/mixed_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/mixed_sink.luac b/lua/ql/test/library-tests/path-alternatives/mixed_sink.luac new file mode 100644 index 000000000000..6192b009a4cc Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/mixed_sink.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/overwrite_controller.lua b/lua/ql/test/library-tests/path-alternatives/overwrite_controller.lua new file mode 100644 index 000000000000..722de33b391d --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/overwrite_controller.lua @@ -0,0 +1,7 @@ +local middle = require("overwrite_middle") + +function source() + return "input" +end + +middle.send(source()) diff --git a/lua/ql/test/library-tests/path-alternatives/overwrite_controller.luac b/lua/ql/test/library-tests/path-alternatives/overwrite_controller.luac new file mode 100644 index 000000000000..8aa0575f8adb Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/overwrite_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/overwrite_middle.lua b/lua/ql/test/library-tests/path-alternatives/overwrite_middle.lua new file mode 100644 index 000000000000..cf0a2062fd56 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/overwrite_middle.lua @@ -0,0 +1,16 @@ +local M = {} +local sink = require("overwrite_sink") + +function shellquote(value) + return value +end + +function M.send(value) + local checked = shellquote(value) + if checked then + checked = value + sink.send(checked) + end +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/overwrite_middle.luac b/lua/ql/test/library-tests/path-alternatives/overwrite_middle.luac new file mode 100644 index 000000000000..f46bd62fa949 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/overwrite_middle.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/overwrite_sink.lua b/lua/ql/test/library-tests/path-alternatives/overwrite_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/overwrite_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/overwrite_sink.luac b/lua/ql/test/library-tests/path-alternatives/overwrite_sink.luac new file mode 100644 index 000000000000..6192b009a4cc Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/overwrite_sink.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/same_route_controller.lua b/lua/ql/test/library-tests/path-alternatives/same_route_controller.lua new file mode 100644 index 000000000000..a333641be3f0 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/same_route_controller.lua @@ -0,0 +1,7 @@ +local middle = require("same_route_middle") + +function source() + return "input" +end + +middle.send(source()) diff --git a/lua/ql/test/library-tests/path-alternatives/same_route_controller.luac b/lua/ql/test/library-tests/path-alternatives/same_route_controller.luac new file mode 100644 index 000000000000..5c08366fc72b Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/same_route_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/same_route_middle.lua b/lua/ql/test/library-tests/path-alternatives/same_route_middle.lua new file mode 100644 index 000000000000..33743c05e3e8 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/same_route_middle.lua @@ -0,0 +1,16 @@ +local M = {} +local sink = require("same_route_sink") + +function shellquote(value) + return value +end + +function M.send(value) + if condition then + sink.send(shellquote(value)) + else + sink.send(value) + end +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/same_route_middle.luac b/lua/ql/test/library-tests/path-alternatives/same_route_middle.luac new file mode 100644 index 000000000000..12c90b74447e Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/same_route_middle.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/same_route_sink.lua b/lua/ql/test/library-tests/path-alternatives/same_route_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/same_route_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/same_route_sink.luac b/lua/ql/test/library-tests/path-alternatives/same_route_sink.luac new file mode 100644 index 000000000000..6192b009a4cc Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/same_route_sink.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.lua b/lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.lua new file mode 100644 index 000000000000..439fadb47350 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.lua @@ -0,0 +1,12 @@ +local M = {} + +function shellquote(value) + return value +end + +function M.clean(value) + local cleaned = shellquote(value) + return cleaned +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.luac b/lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.luac new file mode 100644 index 000000000000..533276eadb1e Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sanitized_controller.lua b/lua/ql/test/library-tests/path-alternatives/sanitized_controller.lua new file mode 100644 index 000000000000..39c02a182a4e --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sanitized_controller.lua @@ -0,0 +1,9 @@ +local cleaner = require("sanitized_cleaner") +local sink = require("sanitized_sink") + +function source() + return "input" +end + +local tainted = source() +sink.send(cleaner.clean(tainted)) diff --git a/lua/ql/test/library-tests/path-alternatives/sanitized_controller.luac b/lua/ql/test/library-tests/path-alternatives/sanitized_controller.luac new file mode 100644 index 000000000000..ef72c48ea426 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sanitized_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sanitized_sink.lua b/lua/ql/test/library-tests/path-alternatives/sanitized_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sanitized_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/sanitized_sink.luac b/lua/ql/test/library-tests/path-alternatives/sanitized_sink.luac new file mode 100644 index 000000000000..6192b009a4cc Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sanitized_sink.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.lua b/lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.lua new file mode 100644 index 000000000000..54e6842e88e9 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.lua @@ -0,0 +1,12 @@ +local M = {} +local sink = require("sibling_return_sink") + +function shellquote(value) + return value +end + +function M.send(value) + sink.send(shellquote(value)) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.luac b/lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.luac new file mode 100644 index 000000000000..d88e5ce62bb6 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_controller.lua b/lua/ql/test/library-tests/path-alternatives/sibling_return_controller.lua new file mode 100644 index 000000000000..8450315f7017 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sibling_return_controller.lua @@ -0,0 +1,11 @@ +local echo = require("sibling_return_echo") +local cleaner = require("sibling_return_cleaner") +local sink = require("sibling_return_sink") + +function source() + return "input" +end + +local tainted = source() +sink.send(tainted) +cleaner.send(echo.copy(tainted)) diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_controller.luac b/lua/ql/test/library-tests/path-alternatives/sibling_return_controller.luac new file mode 100644 index 000000000000..a9c43efb9e09 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sibling_return_controller.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_echo.lua b/lua/ql/test/library-tests/path-alternatives/sibling_return_echo.lua new file mode 100644 index 000000000000..c97ca90afed0 --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sibling_return_echo.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.copy(value) + return value +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_echo.luac b/lua/ql/test/library-tests/path-alternatives/sibling_return_echo.luac new file mode 100644 index 000000000000..36ab16680198 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sibling_return_echo.luac differ diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_sink.lua b/lua/ql/test/library-tests/path-alternatives/sibling_return_sink.lua new file mode 100644 index 000000000000..f1ee8ecccb5f --- /dev/null +++ b/lua/ql/test/library-tests/path-alternatives/sibling_return_sink.lua @@ -0,0 +1,10 @@ +local M = {} + +function execute(command) +end + +function M.send(command) + execute(command) +end + +return M diff --git a/lua/ql/test/library-tests/path-alternatives/sibling_return_sink.luac b/lua/ql/test/library-tests/path-alternatives/sibling_return_sink.luac new file mode 100644 index 000000000000..5325e023f388 Binary files /dev/null and b/lua/ql/test/library-tests/path-alternatives/sibling_return_sink.luac differ diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.expected b/lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.expected new file mode 100644 index 000000000000..09bdf4632164 --- /dev/null +++ b/lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.expected @@ -0,0 +1,5 @@ +| sanitizer-call-edge.active-suppression | non-first sanitizer call-edge route has no active report | +| sanitizer-call-edge.classification | tainted non-first argument crosses the sanitizer call edge into its callee sink | +| sanitizer-call-edge.sibling-active | ordinary sibling call remains active | +| sanitizer-call-edge.typed-call | zero-return doShell call is a typed sanitizer | +| sanitizer-call-edge.unrelated-negative | constant-only sanitizer call does not suppress the tainted sibling route | diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.ql b/lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.ql new file mode 100644 index 000000000000..26fea321687b --- /dev/null +++ b/lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.ql @@ -0,0 +1,45 @@ +import codeql.lua.RulesSanitizerReport + +private string sourceModule() { result = "source.luac" } + +private string sourceRef() { result = "root@pc8:r2" } + +private string sinkModule() { result = "neutral/sink.luac" } + +private string negativeModule() { result = "negative.luac" } + +from string capability, string evidence +where + capability = "sanitizer-call-edge.typed-call" and + sanitizerCall(sourceModule(), "root@pc12", "doShell", "", _) and + evidence = "zero-return doShell call is a typed sanitizer" + or + capability = "sanitizer-call-edge.classification" and + sanitizerClassification(sourceModule(), sourceRef(), sinkModule(), "root.0@pc3:r3", + sourceModule(), "root@pc12", "doShell", "true", "true", "sanitized") and + reportClassification(sourceModule(), sourceRef(), sinkModule(), "root.0@pc3:r3", "sanitized", + "sanitized path suppressed") and + evidence = "tainted non-first argument crosses the sanitizer call edge into its callee sink" + or + capability = "sanitizer-call-edge.active-suppression" and + not exists( + LuaFlowNode source, LuaFlowNode sink, string classification, string reason, string provenance + | + source.getModulePath() = sourceModule() and + source.getValueRef() = sourceRef() and + sink.getModulePath() = sinkModule() and + sink.getValueRef() = "root.0@pc3:r3" and + activeReportPath(source, sink, classification, reason, provenance) + ) and + evidence = "non-first sanitizer call-edge route has no active report" + or + capability = "sanitizer-call-edge.sibling-active" and + reportClassification(negativeModule(), "root@pc8:r2", sinkModule(), "root.1@pc3:r2", + "true-positive", "unsanitized active source-to-sink path") and + evidence = "ordinary sibling call remains active" + or + capability = "sanitizer-call-edge.unrelated-negative" and + not sanitizerClassification(negativeModule(), "root@pc8:r2", sinkModule(), "root.1@pc3:r2", + negativeModule(), "root@pc15", "doShell", "true", "true", "sanitized") and + evidence = "constant-only sanitizer call does not suppress the tainted sibling route" +select capability, evidence diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/negative.lua b/lua/ql/test/library-tests/sanitizer-call-edge/negative.lua new file mode 100644 index 000000000000..50b299198253 --- /dev/null +++ b/lua/ql/test/library-tests/sanitizer-call-edge/negative.lua @@ -0,0 +1,6 @@ +local http = require("neutral.http") +local sink = require("neutral.sink") + +local tainted = http.formvalue("value") +sink.consume(tainted) +sink.doShell("prefix", "constant") diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/negative.luac b/lua/ql/test/library-tests/sanitizer-call-edge/negative.luac new file mode 100644 index 000000000000..db1a95bdb1c5 Binary files /dev/null and b/lua/ql/test/library-tests/sanitizer-call-edge/negative.luac differ diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.lua b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.lua new file mode 100644 index 000000000000..c32ef1257610 --- /dev/null +++ b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.formvalue(name) + return name +end + +return M diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.luac b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.luac new file mode 100644 index 000000000000..d64701f4c67c Binary files /dev/null and b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.luac differ diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.lua b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.lua new file mode 100644 index 000000000000..9d9aab1b7bf0 --- /dev/null +++ b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.lua @@ -0,0 +1,11 @@ +local M = {} + +function M.doShell(prefix, value) + os.execute(value) +end + +function M.consume(value) + os.execute(value) +end + +return M diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.luac b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.luac new file mode 100644 index 000000000000..c6b5f32ac879 Binary files /dev/null and b/lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.luac differ diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/source.lua b/lua/ql/test/library-tests/sanitizer-call-edge/source.lua new file mode 100644 index 000000000000..ced44ebc3cfa --- /dev/null +++ b/lua/ql/test/library-tests/sanitizer-call-edge/source.lua @@ -0,0 +1,5 @@ +local http = require("neutral.http") +local sink = require("neutral.sink") + +local tainted = http.formvalue("value") +sink.doShell("prefix", tainted) diff --git a/lua/ql/test/library-tests/sanitizer-call-edge/source.luac b/lua/ql/test/library-tests/sanitizer-call-edge/source.luac new file mode 100644 index 000000000000..1365c557342a Binary files /dev/null and b/lua/ql/test/library-tests/sanitizer-call-edge/source.luac differ diff --git a/lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.expected b/lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.expected new file mode 100644 index 000000000000..ebe0a8eb93d4 --- /dev/null +++ b/lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.expected @@ -0,0 +1,3 @@ +| alpha.lua | 2 | 35 | 0b96d2df7047463152081fbb3e184e3d0fbbe79b9827c409bf2b89e664f5c296 | +| settings.lua | 6 | 71 | d0d2b92d27f89ad342b16d310af103754a4a9b9cfa33384d4e67f46d8b1a13d4 | +| worker.lua | 5 | 67 | 4e2320b832619f1401499eb462a489ee11b4cb541ad2c34474690c400b8c9a51 | diff --git a/lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.ql b/lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.ql new file mode 100644 index 000000000000..300edf30e24c --- /dev/null +++ b/lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.ql @@ -0,0 +1,5 @@ +import codeql.lua.SourceFile + +from LuaSourceFile file +where file.getBaseName() in ["alpha.lua", "settings.lua", "worker.lua"] +select file.getBaseName(), file.getLineCount(), file.getByteCount(), file.getSha256() diff --git a/lua/ql/test/library-tests/source-file-inventory/alpha.lua b/lua/ql/test/library-tests/source-file-inventory/alpha.lua new file mode 100644 index 000000000000..7eca6a6f2e24 --- /dev/null +++ b/lua/ql/test/library-tests/source-file-inventory/alpha.lua @@ -0,0 +1,2 @@ +local value = "alpha" +return value diff --git a/lua/ql/test/library-tests/source-file-inventory/settings.lua b/lua/ql/test/library-tests/source-file-inventory/settings.lua new file mode 100644 index 000000000000..ace81373fd2f --- /dev/null +++ b/lua/ql/test/library-tests/source-file-inventory/settings.lua @@ -0,0 +1,6 @@ +local settings = { + enabled = true, + retries = 3, +} + +return settings diff --git a/lua/ql/test/library-tests/source-file-inventory/worker.lua b/lua/ql/test/library-tests/source-file-inventory/worker.lua new file mode 100644 index 000000000000..460a573fc479 --- /dev/null +++ b/lua/ql/test/library-tests/source-file-inventory/worker.lua @@ -0,0 +1,5 @@ +local function double(value) + return value * 2 +end + +return double diff --git a/lua/ql/test/qlpack.yml b/lua/ql/test/qlpack.yml new file mode 100644 index 000000000000..23f386df3edb --- /dev/null +++ b/lua/ql/test/qlpack.yml @@ -0,0 +1,7 @@ +name: codeql/lua-tests +groups: [lua, test] +dependencies: + codeql/lua-queries: ${workspace} + codeql/lua-all: ${workspace} +extractor: lua +tests: . diff --git a/lua/tools/autobuild.sh b/lua/tools/autobuild.sh new file mode 100755 index 000000000000..18092679c026 --- /dev/null +++ b/lua/tools/autobuild.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +if [ -n "${CODEQL_LUA_BYTECODE_INPUT_ROOT:-}" ]; then + exec "${CODEQL_DIST}/codeql" database index-files \ + --language=lua \ + --include='**/*.luac' \ + --working-dir="$CODEQL_LUA_BYTECODE_INPUT_ROOT" \ + "$CODEQL_EXTRACTOR_LUA_WIP_DATABASE" +fi + +"${CODEQL_DIST}/codeql" database index-files \ + --language=lua \ + --include='**/*.lua' \ + --working-dir="${LGTM_SRC:-$(pwd)}" \ + "$CODEQL_EXTRACTOR_LUA_WIP_DATABASE" diff --git a/lua/tools/corpus_analyzer.py b/lua/tools/corpus_analyzer.py new file mode 100644 index 000000000000..15e5e79e3630 --- /dev/null +++ b/lua/tools/corpus_analyzer.py @@ -0,0 +1,3361 @@ +"""Analyze an accepted Lua bytecode corpus into immutable generic relations.""" + +from __future__ import annotations + +from collections import defaultdict, deque +from dataclasses import dataclass +from pathlib import PurePosixPath + +from lua_bytecode import Chunk, LoadedArtifact + + +LUA_RK_CONSTANT_BIT = 1 << 8 + + +@dataclass(frozen=True) +class CorpusArtifact: + module_path: str + loaded_artifact: LoadedArtifact + + +@dataclass(frozen=True) +class AcceptedCorpus: + artifacts: tuple[CorpusArtifact, ...] + + +@dataclass(frozen=True) +class ArtifactIdentityRelation: + module_path: str + profile_id: str + + +@dataclass(frozen=True) +class PrototypeIdentityRelation: + module_path: str + prototype_id: str + parent_prototype_id: str + ordinal_index: int + num_params: int + is_vararg: bool + max_stack: int + upvalue_count: int + + +@dataclass(frozen=True) +class InstructionIdentityRelation: + module_path: str + prototype_id: str + pc: int + opcode: str + operand_a: int + operand_b: int + operand_c: int + + +@dataclass(frozen=True) +class ValueIdentityRelation: + module_path: str + prototype_id: str + value_ref: str + value_kind: str + pc: int + slot: int + + +@dataclass(frozen=True) +class ValueFlowRelation: + module_path: str + prototype_id: str + source_ref: str + sink_ref: str + kind: str + provenance: str + + +@dataclass(frozen=True) +class ControlFlowEdgeRelation: + module_path: str + prototype_id: str + source_pc: int + target_pc: int + provenance: str + + +@dataclass(frozen=True) +class DominatorTreeIntervalRelation: + module_path: str + prototype_id: str + pc: int + start: int + end: int + provenance: str + + +@dataclass(frozen=True) +class TableFieldFlowRelation: + module_path: str + prototype_id: str + table_ref: str + field_name: str + write_ref: str + read_ref: str + provenance: str + + +@dataclass(frozen=True) +class GlobalFlowRelation: + module_path: str + prototype_id: str + global_name: str + write_ref: str + read_ref: str + value_ref: str + provenance: str + + +@dataclass(frozen=True) +class UpvalueFlowRelation: + module_path: str + prototype_id: str + upvalue_id: str + capture_ref: str + read_ref: str + write_ref: str + provenance: str + + +@dataclass(frozen=True) +class CallResolutionRelation: + caller_module_path: str + caller_prototype_id: str + callsite_id: str + target_value_ref: str + resolved_name: str + resolution_kind: str + target_module_path: str + target_prototype_id: str + provenance: str + + +@dataclass(frozen=True) +class LiteralRequireRelation: + caller_module_path: str + caller_prototype_id: str + callsite_id: str + require_string: str + argument_ref: str + provenance: str + + +@dataclass(frozen=True) +class ModuleResolutionRelation: + caller_module_path: str + callsite_id: str + require_string: str + status: str + target_module_path: str + reason: str + provenance: str + + +@dataclass(frozen=True) +class ModuleExportRelation: + module_path: str + export_kind: str + field_name: str + value_ref: str + target_prototype_id: str + provenance: str + + +@dataclass(frozen=True) +class InterproceduralFlowRelation: + caller_module_path: str + caller_prototype_id: str + callsite_id: str + callee_module_path: str + callee_prototype_id: str + source_ref: str + sink_ref: str + flow_kind: str + position: int + provenance: str + + +@dataclass(frozen=True) +class AnalysisBoundary: + module_path: str + prototype_id: str + site_id: str + boundary_kind: str + reason: str + provenance: str + + +@dataclass(frozen=True) +class AnalysisResult: + artifact_identities: tuple[ArtifactIdentityRelation, ...] + prototype_identities: tuple[PrototypeIdentityRelation, ...] + instruction_identities: tuple[InstructionIdentityRelation, ...] + value_identities: tuple[ValueIdentityRelation, ...] + value_flows: tuple[ValueFlowRelation, ...] + control_flow_edges: tuple[ControlFlowEdgeRelation, ...] + dominator_tree_intervals: tuple[DominatorTreeIntervalRelation, ...] + table_field_flows: tuple[TableFieldFlowRelation, ...] + global_flows: tuple[GlobalFlowRelation, ...] + upvalue_flows: tuple[UpvalueFlowRelation, ...] + call_resolutions: tuple[CallResolutionRelation, ...] + literal_requires: tuple[LiteralRequireRelation, ...] + module_resolutions: tuple[ModuleResolutionRelation, ...] + module_exports: tuple[ModuleExportRelation, ...] + interprocedural_flows: tuple[InterproceduralFlowRelation, ...] + boundaries: tuple[AnalysisBoundary, ...] + + +def _validate_module_path(module_path: str) -> None: + path = PurePosixPath(module_path) + if ( + not module_path + or "\\" in module_path + or path.is_absolute() + or str(path) != module_path + or ".." in path.parts + ): + raise ValueError(f"module path must be normalized and source-root-relative: {module_path!r}") + + +def _walk_prototypes( + module_path: str, + chunk: Chunk, + prototype_id: str, + parent_prototype_id: str, + ordinal_index: int, + captured_table_by_upvalue: dict[int, str] | None = None, + table_object_values: dict[str, set[str]] | None = None, + table_field_writes: dict[tuple[str, str], set[str]] | None = None, +) -> tuple[ + list[PrototypeIdentityRelation], + list[InstructionIdentityRelation], + list[ValueIdentityRelation], + list[ValueFlowRelation], + list[TableFieldFlowRelation], + list[GlobalFlowRelation], + list[UpvalueFlowRelation], + list[AnalysisBoundary], +]: + if captured_table_by_upvalue is None: + captured_table_by_upvalue = {} + if table_object_values is None: + table_object_values = defaultdict(set) + if table_field_writes is None: + table_field_writes = defaultdict(set) + + prototypes = [ + PrototypeIdentityRelation( + module_path=module_path, + prototype_id=prototype_id, + parent_prototype_id=parent_prototype_id, + ordinal_index=ordinal_index, + num_params=chunk.num_params, + is_vararg=chunk.is_vararg, + max_stack=chunk.max_stack, + upvalue_count=chunk.num_upvalues, + ) + ] + instructions = [ + InstructionIdentityRelation( + module_path=module_path, + prototype_id=prototype_id, + pc=pc, + opcode=instruction.opcode.name, + operand_a=instruction.a, + operand_b=instruction.b, + operand_c=instruction.c, + ) + for pc, instruction in enumerate(chunk.instructions) + ] + values = [ + ValueIdentityRelation( + module_path=module_path, + prototype_id=prototype_id, + value_ref=f"{prototype_id}:r{slot}", + value_kind="entry-register", + pc=-1, + slot=slot, + ) + for slot in range(chunk.num_params) + ] + flows = _prototype_value_flows(module_path, prototype_id, chunk) + table_flows, table_object_flows, captured_table_candidates = _prototype_table_flows( + module_path, + prototype_id, + chunk, + captured_table_by_upvalue, + table_object_values, + table_field_writes, + ) + global_flows = _prototype_global_flows(module_path, prototype_id, chunk) + flows.extend(table_object_flows) + upvalue_flows: list[UpvalueFlowRelation] = [] + boundaries = _prototype_boundaries(module_path, prototype_id, chunk) + + for child_index, child in enumerate(chunk.protos): + child_id = f"{prototype_id}.{child_index}" + child_capture_flows, child_capture_boundaries = _child_upvalue_analysis( + module_path, + prototype_id, + chunk, + child_id, + child, + child_index, + ) + upvalue_flows.extend(child_capture_flows) + boundaries.extend(child_capture_boundaries) + ( + child_prototypes, + child_instructions, + child_values, + child_flows, + child_table_flows, + child_global_flows, + child_upvalue_flows, + child_boundaries, + ) = _walk_prototypes( + module_path, + child, + child_id, + prototype_id, + child_index, + { + upvalue_index: next(iter(table_refs)) + for (candidate_child_index, upvalue_index), table_refs + in captured_table_candidates.items() + if candidate_child_index == child_index and len(table_refs) == 1 + }, + defaultdict( + set, + { + table_ref: set(value_refs) + for table_ref, value_refs in table_object_values.items() + }, + ), + defaultdict( + set, + { + field: set(write_refs) + for field, write_refs in table_field_writes.items() + }, + ), + ) + prototypes.extend(child_prototypes) + instructions.extend(child_instructions) + values.extend(child_values) + flows.extend(child_flows) + table_flows.extend(child_table_flows) + global_flows.extend(child_global_flows) + upvalue_flows.extend(child_upvalue_flows) + boundaries.extend(child_boundaries) + + return ( + prototypes, + instructions, + values, + flows, + table_flows, + global_flows, + upvalue_flows, + boundaries, + ) + + +def _prototype_boundaries( + module_path: str, + prototype_id: str, + chunk: Chunk, +) -> list[AnalysisBoundary]: + boundaries: list[AnalysisBoundary] = [] + for pc, instruction in enumerate(chunk.instructions): + opcode = instruction.opcode.name + kinds_and_reasons: list[tuple[str, str]] = [] + if opcode == "CALL" and instruction.b == 0: + kinds_and_reasons.append( + ( + "open-call-argument-tail", + "only predecessor-proven argument slots are modeled", + ) + ) + if opcode == "TAILCALL" and instruction.b == 0: + kinds_and_reasons.append( + ( + "open-tailcall-argument-tail", + "only predecessor-proven argument slots are modeled", + ) + ) + if opcode == "CALL" and instruction.c == 0: + kinds_and_reasons.append( + ( + "open-call-result-tail", + "only the open producer base result is modeled", + ) + ) + if opcode == "RETURN" and instruction.b == 0: + kinds_and_reasons.append( + ( + "open-return-tail", + "only predecessor-proven return slots are modeled", + ) + ) + if opcode == "VARARG" and instruction.b == 0: + kinds_and_reasons.append( + ( + "open-vararg-tail", + "only the open vararg base result is modeled", + ) + ) + for boundary_kind, reason in kinds_and_reasons: + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=prototype_id, + site_id=f"{prototype_id}@pc{pc}", + boundary_kind=boundary_kind, + reason=reason, + provenance="bytecode-only,open-slot-boundary", + ) + ) + return boundaries + + +def _fixed_instruction_effects( + opcode: str, + operand_a: int, + operand_b: int, + operand_c: int, +) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]] | None: + if opcode == "MOVE": + return (operand_b,), (operand_a,), (operand_b,) + if opcode in {"CLOSURE", "LOADBOOL", "LOADK"}: + return (), (operand_a,), () + if opcode == "LOADNIL": + return (), tuple(range(operand_a, operand_b + 1)), () + if opcode == "NEWTABLE": + return (operand_a,), (operand_a,), (operand_a,) + if opcode in {"GETUPVAL", "GETGLOBAL"}: + return (), (operand_a,), () + if opcode in {"SETGLOBAL", "SETUPVAL"}: + return (operand_a,), (), () + if opcode == "GETTABLE": + reads = (operand_b,) + ( + () if operand_c >= LUA_RK_CONSTANT_BIT else (operand_c,) + ) + return reads, (operand_a,), (operand_b,) + if opcode == "SETTABLE": + reads = (operand_a,) + if operand_b < LUA_RK_CONSTANT_BIT: + reads += (operand_b,) + if operand_c < LUA_RK_CONSTANT_BIT: + reads += (operand_c,) + return reads, (), () + if opcode == "SELF": + reads = (operand_b,) + ( + () if operand_c >= LUA_RK_CONSTANT_BIT else (operand_c,) + ) + return reads, (operand_a, operand_a + 1), reads + if opcode == "CALL": + reads = ( + (operand_a,) + if operand_b == 0 + else tuple(range(operand_a, operand_a + operand_b)) + ) + writes = ( + (operand_a,) + if operand_c == 0 + else tuple(range(operand_a, operand_a + operand_c - 1)) + ) + return reads, writes, tuple(slot for slot in reads if slot > operand_a) + if opcode == "TAILCALL": + if operand_b == 0: + return (operand_a,), (), () + reads = tuple(range(operand_a, operand_a + operand_b)) + return reads, (), tuple(slot for slot in reads if slot > operand_a) + if opcode == "RETURN": + reads = ( + () + if operand_b == 0 + else tuple(range(operand_a, operand_a + operand_b - 1)) + ) + return reads, (), () + if opcode == "VARARG": + writes = ( + (operand_a,) + if operand_b == 0 + else tuple(range(operand_a, operand_a + operand_b - 1)) + ) + return (), writes, () + if opcode in {"ADD", "SUB", "MUL", "DIV", "MOD", "POW"}: + reads = tuple( + slot + for slot in (operand_b, operand_c) + if slot < LUA_RK_CONSTANT_BIT + ) + return reads, (operand_a,), reads + if opcode in {"UNM", "NOT", "LEN"}: + return (operand_b,), (operand_a,), (operand_b,) + if opcode == "CONCAT": + reads = tuple(range(operand_b, operand_c + 1)) + return reads, (operand_a,), reads + if opcode == "SETLIST": + if operand_b == 0: + return None + return tuple(range(operand_a, operand_a + operand_b + 1)), (), () + if opcode in {"EQ", "LT", "LE"}: + reads = tuple( + slot + for slot in (operand_b, operand_c) + if slot < LUA_RK_CONSTANT_BIT + ) + return reads, (), () + if opcode == "TESTSET": + return (operand_b,), (operand_a,), (operand_b,) + if opcode == "TFORLOOP": + reads = (operand_a, operand_a + 1, operand_a + 2) + writes = tuple(range(operand_a + 3, operand_a + 3 + operand_c)) + ( + operand_a + 2, + ) + return reads, writes, (operand_a + 1, operand_a + 2) + if opcode in {"FORLOOP", "FORPREP"}: + return ( + (operand_a, operand_a + 1, operand_a + 2), + (operand_a, operand_a + 2), + (), + ) + if opcode == "TEST": + return (operand_a,), (), () + if opcode == "JMP": + return (), (), () + if opcode == "CLOSE": + return (), (), () + return None + + +def _closure_binding_pcs(chunk: Chunk) -> set[int]: + binding_pcs: set[int] = set() + for closure_pc, instruction in enumerate(chunk.instructions): + if instruction.opcode.name != "CLOSURE" or instruction.b >= len(chunk.protos): + continue + child = chunk.protos[instruction.b] + for offset in range(1, child.num_upvalues + 1): + binding_pc = closure_pc + offset + if binding_pc >= len(chunk.instructions): + break + binding = chunk.instructions[binding_pc] + if binding.opcode.name not in {"MOVE", "GETUPVAL"}: + break + binding_pcs.add(binding_pc) + return binding_pcs + + +def _control_flow_successors( + instructions_by_pc: dict[int, tuple[str, int, int, int]], +) -> dict[int, set[int]]: + pcs = sorted(instructions_by_pc) + pc_set = set(pcs) + successors = {pc: set() for pc in pcs} + for index, pc in enumerate(pcs): + opcode, _, operand_b, _ = instructions_by_pc[pc] + next_pc = pcs[index + 1] if index + 1 < len(pcs) else None + next_next_pc = pcs[index + 2] if index + 2 < len(pcs) else None + if opcode in {"RETURN", "TAILCALL"}: + continue + if opcode == "JMP": + target = pc + 1 + operand_b + if target in pc_set: + successors[pc].add(target) + elif opcode in {"EQ", "LT", "LE", "TEST", "TESTSET", "TFORLOOP"}: + if next_pc is not None: + successors[pc].add(next_pc) + if next_next_pc is not None: + successors[pc].add(next_next_pc) + elif opcode == "FORLOOP": + target = pc + 1 + operand_b + if target in pc_set: + successors[pc].add(target) + if next_pc is not None: + successors[pc].add(next_pc) + elif opcode == "FORPREP": + target = pc + 1 + operand_b + if target in pc_set: + successors[pc].add(target) + elif next_pc is not None: + successors[pc].add(next_pc) + return successors + + +def _control_flow_edges( + instructions: list[InstructionIdentityRelation], +) -> list[ControlFlowEdgeRelation]: + by_prototype: dict[ + tuple[str, str], dict[int, tuple[str, int, int, int]] + ] = defaultdict(dict) + for instruction in instructions: + by_prototype[(instruction.module_path, instruction.prototype_id)][ + instruction.pc + ] = ( + instruction.opcode, + instruction.operand_a, + instruction.operand_b, + instruction.operand_c, + ) + + edges: list[ControlFlowEdgeRelation] = [] + for (module_path, prototype_id), instructions_by_pc in sorted( + by_prototype.items() + ): + for source_pc, targets in sorted( + _control_flow_successors(instructions_by_pc).items() + ): + for target_pc in sorted(targets): + edges.append( + ControlFlowEdgeRelation( + module_path=module_path, + prototype_id=prototype_id, + source_pc=source_pc, + target_pc=target_pc, + provenance="bytecode-only,cfg-successor", + ) + ) + return edges + + +def _reverse_postorder( + successors: dict[int, set[int]], entry: int +) -> list[int]: + visited: set[int] = set() + postorder: list[int] = [] + pending = [(entry, False)] + while pending: + pc, expanded = pending.pop() + if expanded: + postorder.append(pc) + continue + if pc in visited: + continue + visited.add(pc) + pending.append((pc, True)) + for target in sorted(successors.get(pc, set()), reverse=True): + if target not in visited: + pending.append((target, False)) + return list(reversed(postorder)) + + +def _prototype_dominator_tree_intervals( + module_path: str, + prototype_id: str, + instructions_by_pc: dict[int, tuple[str, int, int, int]], +) -> list[DominatorTreeIntervalRelation]: + if not instructions_by_pc: + return [] + # Preserve every feasible static-field definition at CFG joins. + successors = _control_flow_successors(instructions_by_pc) + entry = min(instructions_by_pc) + reverse_postorder = _reverse_postorder(successors, entry) + order = {pc: index for index, pc in enumerate(reverse_postorder)} + predecessors: dict[int, set[int]] = defaultdict(set) + for source, targets in successors.items(): + for target in targets: + predecessors[target].add(source) + + immediate_dominator = {entry: entry} + + def intersect(left: int, right: int) -> int: + while left != right: + while order[left] > order[right]: + left = immediate_dominator[left] + while order[right] > order[left]: + right = immediate_dominator[right] + return left + + changed = True + while changed: + changed = False + for pc in reverse_postorder[1:]: + defined_predecessors = sorted( + predecessor + for predecessor in predecessors.get(pc, set()) + if predecessor in immediate_dominator + ) + if not defined_predecessors: + continue + new_dominator = defined_predecessors[0] + for predecessor in defined_predecessors[1:]: + new_dominator = intersect(predecessor, new_dominator) + if immediate_dominator.get(pc) != new_dominator: + immediate_dominator[pc] = new_dominator + changed = True + + children: dict[int, list[int]] = defaultdict(list) + for pc, dominator in immediate_dominator.items(): + if pc != entry: + children[dominator].append(pc) + intervals: dict[int, tuple[int, int]] = {} + starts: dict[int, int] = {} + next_start = 0 + pending = [(entry, False)] + while pending: + pc, expanded = pending.pop() + if expanded: + intervals[pc] = (starts[pc], next_start - 1) + continue + starts[pc] = next_start + next_start += 1 + pending.append((pc, True)) + for child in sorted(children.get(pc, ()), reverse=True): + pending.append((child, False)) + + return [ + DominatorTreeIntervalRelation( + module_path=module_path, + prototype_id=prototype_id, + pc=pc, + start=start, + end=end, + provenance="bytecode-only,immediate-dominator-tree", + ) + for pc, (start, end) in sorted(intervals.items()) + ] + + +def _dominator_tree_intervals( + instructions: list[InstructionIdentityRelation], +) -> list[DominatorTreeIntervalRelation]: + by_prototype: dict[ + tuple[str, str], dict[int, tuple[str, int, int, int]] + ] = defaultdict(dict) + for instruction in instructions: + by_prototype[(instruction.module_path, instruction.prototype_id)][ + instruction.pc + ] = ( + instruction.opcode, + instruction.operand_a, + instruction.operand_b, + instruction.operand_c, + ) + intervals: list[DominatorTreeIntervalRelation] = [] + for (module_path, prototype_id), instructions_by_pc in sorted( + by_prototype.items() + ): + intervals.extend( + _prototype_dominator_tree_intervals( + module_path, prototype_id, instructions_by_pc + ) + ) + return intervals + + +def _join_states(states: list[dict[int, set[str]]]) -> dict[int, set[str]]: + joined: dict[int, set[str]] = {} + for state in states: + for slot, refs in state.items(): + joined.setdefault(slot, set()).update(refs) + return joined + + +def _state_dependent_effects( + instruction: tuple[str, int, int, int], + fixed_effects: tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]], + state: dict[int, set[str]], + open_top_floor: int | None, +) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]: + opcode, operand_a, operand_b, _ = instruction + reads, writes, dependencies = fixed_effects + if opcode in {"CALL", "TAILCALL"} and operand_b == 0: + open_arguments = ( + () + if open_top_floor is None + else tuple( + sorted( + slot + for slot in state + if slot > operand_a + ) + ) + ) + return (operand_a,) + open_arguments, writes, open_arguments + if opcode == "RETURN" and operand_b == 0: + open_returns = ( + () + if open_top_floor is None + else tuple( + sorted( + slot + for slot in state + if slot >= operand_a and slot >= open_top_floor + ) + ) + ) + return open_returns, writes, dependencies + return fixed_effects + + +def _open_top_floor( + pc: int, + predecessors: dict[int, set[int]], + instructions_by_pc: dict[int, tuple[str, int, int, int]], +) -> int | None: + incoming = predecessors.get(pc, set()) + if len(incoming) != 1: + return None + predecessor = instructions_by_pc[next(iter(incoming))] + opcode, operand_a, operand_b, operand_c = predecessor + if opcode == "CALL" and operand_c == 0: + return operand_a + if opcode == "VARARG" and operand_b == 0: + return operand_a + return None + + +def _prototype_value_flows( + module_path: str, + prototype_id: str, + chunk: Chunk, +) -> list[ValueFlowRelation]: + entry_state = { + slot: {f"{prototype_id}:r{slot}"} + for slot in range(chunk.num_params) + } + effects_by_pc: dict[int, tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]] = {} + instructions_by_pc: dict[int, tuple[str, int, int, int]] = {} + closure_binding_pcs = _closure_binding_pcs(chunk) + for pc, instruction in enumerate(chunk.instructions): + if pc in closure_binding_pcs: + effects = ( + ((instruction.b,), (), ()) + if instruction.opcode.name == "MOVE" + else ((), (), ()) + ) + else: + effects = _fixed_instruction_effects( + instruction.opcode.name, + instruction.a, + instruction.b, + instruction.c, + ) + if effects is None: + break + effects_by_pc[pc] = effects + instructions_by_pc[pc] = ( + instruction.opcode.name, + instruction.a, + instruction.b, + instruction.c, + ) + + if not effects_by_pc: + return [] + + successors = _control_flow_successors(instructions_by_pc) + predecessors: dict[int, set[int]] = defaultdict(set) + for pc, targets in successors.items(): + for target in targets: + predecessors[target].add(pc) + + pcs = sorted(effects_by_pc) + in_states: dict[int, dict[int, set[str]]] = {pc: {} for pc in pcs} + out_states: dict[int, dict[int, set[str]]] = {pc: {} for pc in pcs} + pending = deque(pcs) + queued = set(pcs) + while pending: + pc = pending.popleft() + queued.remove(pc) + incoming = [out_states[pred] for pred in predecessors.get(pc, set())] + if pc == pcs[0]: + incoming.append(entry_state) + joined = _join_states(incoming) + in_states[pc] = joined + next_state = {slot: set(refs) for slot, refs in joined.items()} + opcode, operand_a, operand_b, operand_c = instructions_by_pc[pc] + _, write_slots, _ = effects_by_pc[pc] + if (opcode == "CALL" and operand_c == 0) or ( + opcode == "VARARG" and operand_b == 0 + ): + for slot in tuple(next_state): + if slot >= operand_a: + del next_state[slot] + for slot in write_slots: + write_ref = f"{prototype_id}@pc{pc}:r{slot}" + preserves_prior_definition = opcode == "FORLOOP" or ( + opcode == "CONCAT" and operand_b <= slot <= operand_c + ) + if preserves_prior_definition: + next_state.setdefault(slot, set()).add(write_ref) + else: + next_state[slot] = {write_ref} + if next_state != out_states[pc]: + out_states[pc] = next_state + for target in successors.get(pc, set()): + if target not in queued: + pending.append(target) + queued.add(target) + + flows: list[ValueFlowRelation] = [] + for pc in pcs: + read_slots, write_slots, dependency_slots = _state_dependent_effects( + instructions_by_pc[pc], + effects_by_pc[pc], + in_states[pc], + _open_top_floor(pc, predecessors, instructions_by_pc), + ) + read_refs = { + slot: f"{prototype_id}@pc{pc}:r{slot}" + for slot in read_slots + } + write_refs = { + slot: f"{prototype_id}@pc{pc}:r{slot}" + for slot in write_slots + } + for slot, read_ref in read_refs.items(): + for source_ref in sorted(in_states[pc].get(slot, set())): + flows.append( + ValueFlowRelation( + module_path=module_path, + prototype_id=prototype_id, + source_ref=source_ref, + sink_ref=read_ref, + kind="reaching-definition", + provenance="bytecode-only,cfg-rda", + ) + ) + for slot in dependency_slots: + read_ref = read_refs[slot] + for write_ref in write_refs.values(): + flows.append( + ValueFlowRelation( + module_path=module_path, + prototype_id=prototype_id, + source_ref=read_ref, + sink_ref=write_ref, + kind="same-instruction-dependence", + provenance="bytecode-only,cfg-rda", + ) + ) + return flows + + +def _constant_string(chunk: Chunk, index: int) -> str | None: + if index >= len(chunk.constants): + return None + constant = chunk.constants[index] + if constant.type_name != "string": + return None + return constant.text() + + +def _constant_string_operand(chunk: Chunk, operand: int) -> str | None: + if operand < LUA_RK_CONSTANT_BIT: + return None + return _constant_string(chunk, operand - LUA_RK_CONSTANT_BIT) + + +def _prototype_global_flows( + module_path: str, + prototype_id: str, + chunk: Chunk, +) -> list[GlobalFlowRelation]: + global_writes: dict[str, str] = {} + flows: list[GlobalFlowRelation] = [] + + for pc, instruction in enumerate(chunk.instructions): + opcode = instruction.opcode.name + if opcode == "SETGLOBAL": + global_name = _constant_string(chunk, instruction.b) + if global_name: + global_writes[global_name] = f"{prototype_id}@pc{pc}:r{instruction.a}" + elif opcode == "GETGLOBAL": + global_name = _constant_string(chunk, instruction.b) + if global_name and global_name in global_writes: + write_ref = global_writes[global_name] + flows.append( + GlobalFlowRelation( + module_path=module_path, + prototype_id=prototype_id, + global_name=global_name, + write_ref=write_ref, + read_ref=f"{prototype_id}@pc{pc}:r{instruction.a}", + value_ref=write_ref, + provenance="bytecode-only,precise-global-state", + ) + ) + + return flows + + +def _child_upvalue_analysis( + module_path: str, + parent_prototype_id: str, + parent_chunk: Chunk, + child_prototype_id: str, + child_chunk: Chunk, + child_index: int, +) -> tuple[list[UpvalueFlowRelation], list[AnalysisBoundary]]: + first_write_by_upvalue: dict[int, str] = {} + for pc, instruction in enumerate(child_chunk.instructions): + if instruction.opcode.name == "SETUPVAL": + first_write_by_upvalue.setdefault( + instruction.b, + f"{child_prototype_id}@pc{pc}:r{instruction.a}", + ) + + flows: list[UpvalueFlowRelation] = [] + boundaries: list[AnalysisBoundary] = [] + for closure_pc, closure in enumerate(parent_chunk.instructions): + if closure.opcode.name != "CLOSURE" or closure.b != child_index: + continue + for upvalue_index in range(child_chunk.num_upvalues): + binding_pc = closure_pc + 1 + upvalue_index + if binding_pc >= len(parent_chunk.instructions): + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=parent_prototype_id, + site_id=f"{parent_prototype_id}@pc{closure_pc}:u{upvalue_index}", + boundary_kind="malformed-upvalue-capture", + reason=( + f"upvalue {upvalue_index} binding after " + f"{parent_prototype_id}@pc{closure_pc} is missing" + ), + provenance="bytecode-only,upvalue-capture-boundary", + ) + ) + continue + binding = parent_chunk.instructions[binding_pc] + if binding.opcode.name not in {"MOVE", "GETUPVAL"}: + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=parent_prototype_id, + site_id=f"{parent_prototype_id}@pc{closure_pc}:u{upvalue_index}", + boundary_kind="malformed-upvalue-capture", + reason=( + f"upvalue {upvalue_index} binding at " + f"{parent_prototype_id}@pc{binding_pc} must use " + "MOVE or GETUPVAL" + ), + provenance="bytecode-only,upvalue-capture-boundary", + ) + ) + continue + capture_slot = binding.b if binding.opcode.name == "MOVE" else binding.a + capture_ref = f"{parent_prototype_id}@pc{binding_pc}:r{capture_slot}" + upvalue_id = f"{child_prototype_id}:u{upvalue_index}" + write_ref = first_write_by_upvalue.get(upvalue_index, "") + for read_pc, instruction in enumerate(child_chunk.instructions): + if ( + instruction.opcode.name == "GETUPVAL" + and instruction.b == upvalue_index + ): + flows.append( + UpvalueFlowRelation( + module_path=module_path, + prototype_id=child_prototype_id, + upvalue_id=upvalue_id, + capture_ref=capture_ref, + read_ref=( + f"{child_prototype_id}@pc{read_pc}:r{instruction.a}" + ), + write_ref=write_ref, + provenance="bytecode-only,derived-upvalue-flow", + ) + ) + return flows, boundaries + + +def _prototype_table_flows( + module_path: str, + prototype_id: str, + chunk: Chunk, + captured_table_by_upvalue: dict[int, str], + object_values: dict[str, set[str]], + field_writes: dict[tuple[str, str], set[str]], +) -> tuple[ + list[TableFieldFlowRelation], + list[ValueFlowRelation], + dict[tuple[int, int], set[str]], +]: + entry_field_writes = { + field: set(write_refs) for field, write_refs in field_writes.items() + } + table_objects: dict[int, str] = {} + captured_table_candidates: dict[tuple[int, int], set[str]] = defaultdict(set) + object_flows: list[ValueFlowRelation] = [] + instructions_by_pc: dict[int, tuple[str, int, int, int]] = {} + field_write_by_pc: dict[ + int, tuple[tuple[str, str], str | None] + ] = {} + field_read_by_pc: dict[int, tuple[tuple[str, str], str]] = {} + closure_binding_pcs = _closure_binding_pcs(chunk) + closure_binding_sites: dict[int, tuple[int, int]] = {} + for closure_pc, closure in enumerate(chunk.instructions): + if closure.opcode.name != "CLOSURE" or closure.b >= len(chunk.protos): + continue + for upvalue_index in range(chunk.protos[closure.b].num_upvalues): + closure_binding_sites[closure_pc + upvalue_index + 1] = ( + closure.b, + upvalue_index, + ) + + for pc, instruction in enumerate(chunk.instructions): + opcode = instruction.opcode.name + is_closure_binding = pc in closure_binding_pcs + if is_closure_binding: + table_ref = ( + table_objects.get(instruction.b) + if opcode == "MOVE" + else captured_table_by_upvalue.get(instruction.b) + if opcode == "GETUPVAL" + else None + ) + if table_ref is not None: + captured_table_candidates[closure_binding_sites[pc]].add(table_ref) + if is_closure_binding: + effects = ( + ((instruction.b,), (), ()) + if opcode == "MOVE" + else ((), (), ()) + ) + else: + effects = _fixed_instruction_effects( + opcode, + instruction.a, + instruction.b, + instruction.c, + ) + if effects is None: + break + instructions_by_pc[pc] = ( + opcode, + instruction.a, + instruction.b, + instruction.c, + ) + + moved_table_ref = ( + table_objects.get(instruction.b) + if opcode == "MOVE" and not is_closure_binding + else None + ) + if opcode == "SETTABLE" and instruction.a not in table_objects: + table_objects[instruction.a] = ( + f"{prototype_id}@pc{pc}:r{instruction.a}" + ) + for slot in effects[0]: + table_ref = table_objects.get(slot) + if table_ref is None: + continue + for source_ref in sorted(object_values.get(table_ref, set())): + object_flows.append( + ValueFlowRelation( + module_path=module_path, + prototype_id=prototype_id, + source_ref=source_ref, + sink_ref=f"{prototype_id}@pc{pc}:r{slot}", + kind="table-object-dependence", + provenance="bytecode-only,mutable-table-object", + ) + ) + + if opcode == "SETTABLE": + table_ref = table_objects.get(instruction.a) + field_name = _constant_string_operand(chunk, instruction.b) + field_key = ( + (table_ref, field_name) + if table_ref is not None and field_name is not None + else None + ) + if field_key is not None: + field_write_by_pc[pc] = (field_key, None) + if table_ref is not None and instruction.c < LUA_RK_CONSTANT_BIT: + value_ref = f"{prototype_id}@pc{pc}:r{instruction.c}" + object_values[table_ref].add(value_ref) + if field_key is not None: + field_write_by_pc[pc] = (field_key, value_ref) + elif opcode == "SETLIST": + table_ref = table_objects.get(instruction.a) + if table_ref is not None: + for slot in range(instruction.a + 1, instruction.a + instruction.b + 1): + object_values[table_ref].add(f"{prototype_id}@pc{pc}:r{slot}") + elif opcode == "GETTABLE": + table_ref = table_objects.get(instruction.b) + field_name = _constant_string_operand(chunk, instruction.c) + if table_ref is not None and field_name is not None: + field_read_by_pc[pc] = ( + (table_ref, field_name), + f"{prototype_id}@pc{pc}:r{instruction.a}", + ) + + for slot in effects[1]: + table_objects.pop(slot, None) + if opcode == "NEWTABLE": + table_objects[instruction.a] = f"{prototype_id}@pc{pc}:r{instruction.a}" + elif moved_table_ref is not None: + table_objects[instruction.a] = moved_table_ref + elif opcode == "GETUPVAL" and not is_closure_binding: + captured_table_ref = captured_table_by_upvalue.get(instruction.b) + if captured_table_ref is not None: + table_objects[instruction.a] = captured_table_ref + + successors = _control_flow_successors(instructions_by_pc) + predecessors: dict[int, set[int]] = defaultdict(set) + for source_pc, target_pcs in successors.items(): + for target_pc in target_pcs: + predecessors[target_pc].add(source_pc) + + pcs = sorted(instructions_by_pc) + in_states: dict[int, dict[tuple[str, str], set[str]]] = { + pc: {} for pc in pcs + } + out_states: dict[int, dict[tuple[str, str], set[str]]] = { + pc: {} for pc in pcs + } + pending = deque(pcs) + queued = set(pcs) + while pending: + pc = pending.popleft() + queued.remove(pc) + incoming = [out_states[pred] for pred in predecessors.get(pc, set())] + if pc == pcs[0]: + incoming.append(entry_field_writes) + joined: dict[tuple[str, str], set[str]] = defaultdict(set) + for state in incoming: + for field_key, write_refs in state.items(): + joined[field_key].update(write_refs) + in_states[pc] = dict(joined) + next_state = { + field_key: set(write_refs) + for field_key, write_refs in joined.items() + } + field_write = field_write_by_pc.get(pc) + if field_write is not None: + field_key, write_ref = field_write + next_state[field_key] = {write_ref} if write_ref is not None else set() + if next_state != out_states[pc]: + out_states[pc] = next_state + for target_pc in successors.get(pc, set()): + if target_pc not in queued: + pending.append(target_pc) + queued.add(target_pc) + + field_flows: list[TableFieldFlowRelation] = [] + for pc, (field_key, read_ref) in sorted(field_read_by_pc.items()): + table_ref, field_name = field_key + for write_ref in sorted(in_states[pc].get(field_key, set())): + field_flows.append( + TableFieldFlowRelation( + module_path=module_path, + prototype_id=prototype_id, + table_ref=table_ref, + field_name=field_name, + write_ref=write_ref, + read_ref=read_ref, + provenance="bytecode-only,precise-table-field", + ) + ) + + exit_state: dict[tuple[str, str], set[str]] = defaultdict(set) + for pc in pcs: + if not successors.get(pc): + for field_key, write_refs in out_states[pc].items(): + exit_state[field_key].update(write_refs) + field_writes.clear() + for field_key, write_refs in exit_state.items(): + if write_refs: + field_writes[field_key].update(write_refs) + + return field_flows, object_flows, captured_table_candidates + + +def _chunks_by_prototype(root_chunk: Chunk) -> dict[str, Chunk]: + chunks: dict[str, Chunk] = {} + pending = [("root", root_chunk)] + while pending: + prototype_id, chunk = pending.pop() + chunks[prototype_id] = chunk + pending.extend( + (f"{prototype_id}.{child_index}", child) + for child_index, child in enumerate(chunk.protos) + ) + return chunks + + +def _closure_debug_local_name( + instruction: InstructionIdentityRelation, + chunk: Chunk, +) -> str | None: + if instruction.opcode != "CLOSURE": + return None + activation_pc = instruction.pc + 1 + active_locals = [ + local + for local in chunk.locals + if local.start <= activation_pc < local.end + ] + if instruction.operand_a >= len(active_locals): + return None + local = active_locals[instruction.operand_a] + if local.start != activation_pc or not local.name: + return None + return local.name + + +def _reaching_global_member_names( + call: InstructionIdentityRelation, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], + call_result_name_by_ref: dict[str, str], +) -> set[str]: + target_ref = f"{call.prototype_id}@pc{call.pc}:r{call.operand_a}" + + def names_for_ref(source_ref: str, active: frozenset[str]) -> set[str]: + if source_ref in active: + return set() + call_result_name = call_result_name_by_ref.get(source_ref) + if call_result_name is not None: + return {call_result_name} + instruction = instruction_by_result_ref.get(source_ref) + if instruction is None: + return set() + chunk = chunks_by_prototype[instruction.prototype_id] + if instruction.opcode == "GETGLOBAL": + name = _constant_string(chunk, instruction.operand_b) + return {name} if name is not None else set() + if instruction.opcode == "CLOSURE": + name = _closure_debug_local_name(instruction, chunk) + return {name} if name is not None else set() + if instruction.opcode == "MOVE": + base_slot = instruction.operand_b + member_name = None + elif instruction.opcode == "GETTABLE": + base_slot = instruction.operand_b + member_name = _constant_string_operand(chunk, instruction.operand_c) + if member_name is None: + return set() + else: + return set() + base_read_ref = ( + f"{instruction.prototype_id}@pc{instruction.pc}:r{base_slot}" + ) + base_names = { + name + for base_ref in reaching_sources_by_ref.get(base_read_ref, set()) + for name in names_for_ref(base_ref, active | {source_ref}) + } + if member_name is None: + return base_names + return {f"{base_name}.{member_name}" for base_name in base_names} + + return { + name + for source_ref in reaching_sources_by_ref.get(target_ref, set()) + for name in names_for_ref(source_ref, frozenset()) + } + + +def _reaching_global_names( + call: InstructionIdentityRelation, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], +) -> set[str]: + target_ref = f"{call.prototype_id}@pc{call.pc}:r{call.operand_a}" + names: set[str] = set() + for global_ref in reaching_sources_by_ref.get(target_ref, set()): + global_instruction = instruction_by_result_ref.get(global_ref) + if global_instruction is None or global_instruction.opcode != "GETGLOBAL": + continue + chunk = chunks_by_prototype[global_instruction.prototype_id] + global_name = _constant_string(chunk, global_instruction.operand_b) + if global_name is not None: + names.add(global_name) + return names + + +def _reaching_upvalue_member_names( + call: InstructionIdentityRelation, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], + upvalue_flows_by_read_ref: dict[str, list[UpvalueFlowRelation]], + call_result_name_by_ref: dict[str, str], +) -> set[str]: + def captured_names_for_read( + upvalue_ref: str, + visiting: frozenset[str] = frozenset(), + ) -> set[str]: + if upvalue_ref in visiting: + return set() + upvalue_instruction = instruction_by_result_ref.get(upvalue_ref) + if upvalue_instruction is None or upvalue_instruction.opcode != "GETUPVAL": + return set() + next_visiting = visiting | {upvalue_ref} + captured_names: set[str] = set() + for flow in upvalue_flows_by_read_ref.get(upvalue_ref, []): + if ( + flow.prototype_id != upvalue_instruction.prototype_id or + flow.read_ref != upvalue_ref + ): + continue + if flow.write_ref: + write_pc = int( + flow.write_ref.rsplit("@pc", 1)[1].split(":", 1)[0] + ) + if write_pc < upvalue_instruction.pc: + continue + capture_sources = reaching_sources_by_ref.get( + flow.capture_ref, + set(), + ) | {flow.capture_ref} + for source_ref in capture_sources: + captured_name = call_result_name_by_ref.get(source_ref) + if captured_name is not None: + captured_names.add(captured_name) + captured_names.update( + captured_names_for_read(source_ref, next_visiting) + ) + if captured_names: + return captured_names + upvalue_index = upvalue_instruction.operand_b + upvalue_chunk = chunks_by_prototype[upvalue_instruction.prototype_id] + if 0 <= upvalue_index < len(upvalue_chunk.upvalues): + upvalue_name = upvalue_chunk.upvalues[upvalue_index] + if upvalue_name: + return {upvalue_name} + return set() + + target_ref = f"{call.prototype_id}@pc{call.pc}:r{call.operand_a}" + names: set[str] = set() + for member_ref in reaching_sources_by_ref.get(target_ref, set()): + member_instruction = instruction_by_result_ref.get(member_ref) + if member_instruction is None or member_instruction.opcode != "GETTABLE": + continue + chunk = chunks_by_prototype[member_instruction.prototype_id] + member_name = _constant_string_operand(chunk, member_instruction.operand_c) + if member_name is None: + continue + base_read_ref = ( + f"{member_instruction.prototype_id}@pc{member_instruction.pc}:" + f"r{member_instruction.operand_b}" + ) + for upvalue_ref in reaching_sources_by_ref.get(base_read_ref, set()): + upvalue_instruction = instruction_by_result_ref.get(upvalue_ref) + if upvalue_instruction is None or upvalue_instruction.opcode != "GETUPVAL": + continue + captured_names = captured_names_for_read(upvalue_ref) + if len(captured_names) == 1: + names.add(f"{next(iter(captured_names))}.{member_name}") + continue + if captured_names: + continue + return names + + +def _literal_require_result_name( + call: InstructionIdentityRelation, + resolved_name: str, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], +) -> str | None: + if resolved_name != "require": + return resolved_name + argument_ref = f"{call.prototype_id}@pc{call.pc}:r{call.operand_a + 1}" + sources = reaching_sources_by_ref.get(argument_ref, set()) + if len(sources) != 1: + return None + source_instruction = instruction_by_result_ref.get(next(iter(sources))) + if source_instruction is None or source_instruction.opcode != "LOADK": + return None + return _constant_string( + chunks_by_prototype[source_instruction.prototype_id], + source_instruction.operand_b, + ) + + +def _record_call_result_names( + call: InstructionIdentityRelation, + resolved_name: str, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], + call_result_name_by_ref: dict[str, str], +) -> None: + if call.opcode != "CALL" or call.operand_c in {0, 1}: + return + if resolved_name == "require" and call.operand_c != 2: + return + result_name = _literal_require_result_name( + call, + resolved_name, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + ) + if result_name is not None: + result_end = call.operand_a + call.operand_c - 1 + for result_slot in range(call.operand_a, result_end): + call_result_name_by_ref[ + f"{call.prototype_id}@pc{call.pc}:r{result_slot}" + ] = result_name + + +def _iterator_result_names( + result_ref: str, + reaching_sources_by_ref: dict[str, set[str]], + call_result_name_by_ref: dict[str, str], + iterator_target_ref_by_result_ref: dict[str, str], + visited: set[str], +) -> set[str]: + direct_name = call_result_name_by_ref.get(result_ref) + if direct_name is not None: + return {direct_name} + if result_ref in visited: + return set() + target_ref = iterator_target_ref_by_result_ref.get(result_ref) + if target_ref is None: + return set() + next_visited = visited | {result_ref} + names = set().union( + *( + _iterator_result_names( + source_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + iterator_target_ref_by_result_ref, + next_visited, + ) + for source_ref in reaching_sources_by_ref.get(target_ref, set()) + ) + ) + if len(names) == 1: + call_result_name_by_ref[result_ref] = next(iter(names)) + return names + return set() + + +def _reaching_self_parameter_names( + call: InstructionIdentityRelation, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], + parameter_name_by_ref: dict[str, str], +) -> set[str]: + target_ref = f"{call.prototype_id}@pc{call.pc}:r{call.operand_a}" + names: set[str] = set() + for self_ref in reaching_sources_by_ref.get(target_ref, set()): + self_instruction = instruction_by_result_ref.get(self_ref) + if self_instruction is None or self_instruction.opcode != "SELF": + continue + chunk = chunks_by_prototype[self_instruction.prototype_id] + member_name = _constant_string_operand(chunk, self_instruction.operand_c) + if member_name is None: + continue + base_read_ref = ( + f"{self_instruction.prototype_id}@pc{self_instruction.pc}:" + f"r{self_instruction.operand_b}" + ) + for base_ref in reaching_sources_by_ref.get(base_read_ref, set()): + base_name = parameter_name_by_ref.get(base_ref) + if base_name is not None: + names.add(f"{base_name}.{member_name}") + return names + + +def _reaching_self_call_result_names( + call: InstructionIdentityRelation, + chunks_by_prototype: dict[str, Chunk], + instruction_by_result_ref: dict[str, InstructionIdentityRelation], + reaching_sources_by_ref: dict[str, set[str]], + call_result_name_by_ref: dict[str, str], + iterator_target_ref_by_result_ref: dict[str, str], +) -> set[str]: + target_ref = f"{call.prototype_id}@pc{call.pc}:r{call.operand_a}" + names: set[str] = set() + for self_ref in reaching_sources_by_ref.get(target_ref, set()): + self_instruction = instruction_by_result_ref.get(self_ref) + if self_instruction is None or self_instruction.opcode != "SELF": + continue + chunk = chunks_by_prototype[self_instruction.prototype_id] + member_name = _constant_string_operand(chunk, self_instruction.operand_c) + if member_name is None: + continue + base_read_ref = ( + f"{self_instruction.prototype_id}@pc{self_instruction.pc}:" + f"r{self_instruction.operand_b}" + ) + for base_ref in reaching_sources_by_ref.get(base_read_ref, set()): + for base_name in _iterator_result_names( + base_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + iterator_target_ref_by_result_ref, + set(), + ): + names.add(f"{base_name}.{member_name}") + return names + + +def _artifact_call_resolutions( + module_path: str, + root_chunk: Chunk, + prototypes: list[PrototypeIdentityRelation], + instructions: list[InstructionIdentityRelation], + flows: list[ValueFlowRelation], + table_flows: list[TableFieldFlowRelation], + global_flows: list[GlobalFlowRelation], + upvalue_flows: list[UpvalueFlowRelation], +) -> tuple[list[CallResolutionRelation], list[AnalysisBoundary]]: + chunks_by_prototype = _chunks_by_prototype(root_chunk) + prototype_ids = {prototype.prototype_id for prototype in prototypes} + parameter_name_by_ref = { + f"{prototype.prototype_id}:r{slot}": f"Param_{slot}" + for prototype in prototypes + for slot in range(prototype.num_params) + } + closure_targets = { + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}": target_prototype_id + for instruction in instructions + if instruction.opcode == "CLOSURE" + for target_prototype_id in ( + f"{instruction.prototype_id}.{instruction.operand_b}", + ) + if target_prototype_id in prototype_ids + } + adjacency: dict[str, set[tuple[str, str]]] = defaultdict(set) + reaching_sources_by_ref: dict[str, set[str]] = defaultdict(set) + for flow in flows: + if flow.kind == "reaching-definition": + adjacency[flow.source_ref].add((flow.sink_ref, "")) + reaching_sources_by_ref[flow.sink_ref].add(flow.source_ref) + for instruction in instructions: + if instruction.opcode == "MOVE": + adjacency[ + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_b}" + ].add( + ( + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}", + "move", + ) + ) + for flow in table_flows: + adjacency[flow.write_ref].add((flow.read_ref, "table-field")) + for flow in global_flows: + adjacency[flow.write_ref].add((flow.read_ref, "global")) + upvalue_read_pcs = { + ( + instruction.prototype_id, + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}", + ): instruction.pc + for instruction in instructions + if instruction.opcode == "GETUPVAL" + } + upvalue_write_pcs = { + ( + instruction.prototype_id, + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}", + ): instruction.pc + for instruction in instructions + if instruction.opcode == "SETUPVAL" + } + for flow in upvalue_flows: + read_pc = upvalue_read_pcs[(flow.prototype_id, flow.read_ref)] + source_ref = flow.capture_ref + if flow.write_ref: + write_pc = upvalue_write_pcs[(flow.prototype_id, flow.write_ref)] + if write_pc < read_pc: + source_ref = flow.write_ref + adjacency[source_ref].add((flow.read_ref, "upvalue")) + instruction_by_result_ref = { + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}": instruction + for instruction in instructions + if instruction.opcode in { + "GETGLOBAL", + "GETUPVAL", + "GETTABLE", + "SELF", + "MOVE", + "CLOSURE", + "LOADK", + } + } + + target_states_by_ref: dict[str, set[tuple[str, frozenset[str]]]] = { + closure_ref: {(target_prototype, frozenset())} + for closure_ref, target_prototype in closure_targets.items() + } + pending = deque(target_states_by_ref) + queued = set(target_states_by_ref) + while pending: + source_ref = pending.popleft() + queued.remove(source_ref) + source_states = target_states_by_ref[source_ref] + for sink_ref, carrier_kind in adjacency.get(source_ref, set()): + next_states = { + ( + target_prototype, + carrier_kinds | ({carrier_kind} if carrier_kind else set()), + ) + for target_prototype, carrier_kinds in source_states + } + sink_states = target_states_by_ref.setdefault(sink_ref, set()) + previous_count = len(sink_states) + sink_states.update(next_states) + if len(sink_states) != previous_count and sink_ref not in queued: + pending.append(sink_ref) + queued.add(sink_ref) + + parameter_derived_refs = set(parameter_name_by_ref) + pending_parameter_refs = deque(parameter_derived_refs) + while pending_parameter_refs: + source_ref = pending_parameter_refs.popleft() + for sink_ref, _ in adjacency.get(source_ref, set()): + if sink_ref in parameter_derived_refs: + continue + parameter_derived_refs.add(sink_ref) + pending_parameter_refs.append(sink_ref) + + resolutions: list[CallResolutionRelation] = [] + boundaries: list[AnalysisBoundary] = [] + call_result_name_by_ref: dict[str, str] = {} + iterator_target_ref_by_result_ref = { + f"{instruction.prototype_id}@pc{instruction.pc}:r{result_slot}": ( + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}" + ) + for instruction in instructions + if instruction.opcode == "TFORLOOP" + for result_slot in range( + instruction.operand_a + 3, + instruction.operand_a + 3 + instruction.operand_c, + ) + } + upvalue_flows_by_read_ref: dict[str, list[UpvalueFlowRelation]] = defaultdict(list) + for flow in upvalue_flows: + upvalue_flows_by_read_ref[flow.read_ref].append(flow) + for instruction in instructions: + if instruction.opcode not in {"CALL", "TAILCALL"}: + continue + target_value_ref = ( + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}" + ) + target_states = target_states_by_ref.get(target_value_ref, set()) + targets = {target_prototype for target_prototype, _ in target_states} + resolved_names = set().union( + _reaching_global_names( + instruction, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + ), + _reaching_global_member_names( + instruction, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + ), + _reaching_upvalue_member_names( + instruction, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + upvalue_flows_by_read_ref, + call_result_name_by_ref, + ), + _reaching_self_parameter_names( + instruction, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + parameter_name_by_ref, + ), + _reaching_self_call_result_names( + instruction, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + iterator_target_ref_by_result_ref, + ), + ) + if len(targets) == 1: + carrier_kinds = set().union( + *(state_carriers for _, state_carriers in target_states) + ) + if "upvalue" in carrier_kinds: + resolution_kind = "closure-upvalue" + elif "global" in carrier_kinds: + resolution_kind = "closure-global" + elif "table-field" in carrier_kinds: + resolution_kind = "closure-table-field" + elif "move" in carrier_kinds: + resolution_kind = "closure-move" + else: + resolution_kind = "direct-closure" + resolved_name = ( + next(iter(resolved_names)) if len(resolved_names) == 1 else "" + ) + resolutions.append( + CallResolutionRelation( + caller_module_path=module_path, + caller_prototype_id=instruction.prototype_id, + callsite_id=f"{instruction.prototype_id}@pc{instruction.pc}", + target_value_ref=target_value_ref, + resolved_name=resolved_name, + resolution_kind=resolution_kind, + target_module_path=module_path, + target_prototype_id=next(iter(targets)), + provenance=f"bytecode-only,{resolution_kind}-target", + ) + ) + if resolved_name: + _record_call_result_names( + instruction, + resolved_name, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + ) + continue + if len(targets) > 1: + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=instruction.prototype_id, + site_id=f"{instruction.prototype_id}@pc{instruction.pc}", + boundary_kind="unresolved-call-target", + reason="multiple-candidates", + provenance="bytecode-only,call-resolution-boundary", + ) + ) + continue + + chunk = chunks_by_prototype[instruction.prototype_id] + if instruction.pc > 0: + previous = chunk.instructions[instruction.pc - 1] + if previous.opcode.name == "GETGLOBAL" and previous.a == instruction.operand_a: + resolved_name = _constant_string(chunk, previous.b) + if resolved_name is not None: + resolutions.append( + CallResolutionRelation( + caller_module_path=module_path, + caller_prototype_id=instruction.prototype_id, + callsite_id=f"{instruction.prototype_id}@pc{instruction.pc}", + target_value_ref=target_value_ref, + resolved_name=resolved_name, + resolution_kind="derived-adjacent-global-name", + target_module_path="", + target_prototype_id="", + provenance="bytecode-only,derived-call-target", + ) + ) + _record_call_result_names( + instruction, + resolved_name, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + ) + continue + + if len(resolved_names) > 1: + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=instruction.prototype_id, + site_id=f"{instruction.prototype_id}@pc{instruction.pc}", + boundary_kind="unresolved-call-target", + reason="multiple-candidates", + provenance="bytecode-only,call-resolution-boundary", + ) + ) + continue + resolved_name = next(iter(resolved_names), None) + if resolved_name is None: + reason = ( + "param-derived" + if not target_states and target_value_ref in parameter_derived_refs + else "no-proven-target" + ) + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=instruction.prototype_id, + site_id=f"{instruction.prototype_id}@pc{instruction.pc}", + boundary_kind="unresolved-call-target", + reason=reason, + provenance="bytecode-only,call-resolution-boundary", + ) + ) + continue + resolutions.append( + CallResolutionRelation( + caller_module_path=module_path, + caller_prototype_id=instruction.prototype_id, + callsite_id=f"{instruction.prototype_id}@pc{instruction.pc}", + target_value_ref=target_value_ref, + resolved_name=resolved_name, + resolution_kind="derived-reaching-definition-call-target", + target_module_path="", + target_prototype_id="", + provenance="bytecode-only,derived-call-target,reaching-definition", + ) + ) + _record_call_result_names( + instruction, + resolved_name, + chunks_by_prototype, + instruction_by_result_ref, + reaching_sources_by_ref, + call_result_name_by_ref, + ) + return resolutions, boundaries + + +def _artifact_literal_requires( + module_path: str, + root_chunk: Chunk, + instructions: list[InstructionIdentityRelation], + flows: list[ValueFlowRelation], + call_resolutions: list[CallResolutionRelation], +) -> tuple[list[LiteralRequireRelation], list[AnalysisBoundary]]: + chunks_by_prototype = _chunks_by_prototype(root_chunk) + calls_by_site = { + (instruction.prototype_id, f"{instruction.prototype_id}@pc{instruction.pc}"): instruction + for instruction in instructions + if instruction.opcode in {"CALL", "TAILCALL"} + } + loadk_by_result_ref = { + f"{instruction.prototype_id}@pc{instruction.pc}:r{instruction.operand_a}": instruction + for instruction in instructions + if instruction.opcode == "LOADK" + } + reaching_sources_by_ref: dict[str, set[str]] = defaultdict(set) + for flow in flows: + if flow.kind == "reaching-definition": + reaching_sources_by_ref[flow.sink_ref].add(flow.source_ref) + + literal_requires: list[LiteralRequireRelation] = [] + boundaries: list[AnalysisBoundary] = [] + for resolution in call_resolutions: + if resolution.resolved_name != "require": + continue + call = calls_by_site[ + (resolution.caller_prototype_id, resolution.callsite_id) + ] + argument_ref = ( + f"{call.prototype_id}@pc{call.pc}:r{call.operand_a + 1}" + ) + sources = reaching_sources_by_ref.get(argument_ref, set()) + source = next(iter(sources)) if len(sources) == 1 else None + loadk = loadk_by_result_ref.get(source) if source is not None else None + require_string = ( + _constant_string(chunks_by_prototype[loadk.prototype_id], loadk.operand_b) + if loadk is not None + else None + ) + if require_string is not None: + literal_requires.append( + LiteralRequireRelation( + caller_module_path=module_path, + caller_prototype_id=call.prototype_id, + callsite_id=resolution.callsite_id, + require_string=require_string, + argument_ref=argument_ref, + provenance="bytecode-only,derived-literal-require", + ) + ) + continue + reason = "multiple-candidates" if len(sources) > 1 else "dynamic-argument" + boundaries.append( + AnalysisBoundary( + module_path=module_path, + prototype_id=call.prototype_id, + site_id=resolution.callsite_id, + boundary_kind="unresolved-literal-require", + reason=reason, + provenance="bytecode-only,literal-require-boundary", + ) + ) + return literal_requires, boundaries + + +def _module_names_for_path(module_path: str) -> set[str]: + path = module_path.removesuffix(".luac").removesuffix(".lua") + parts = path.split("/") + names: set[str] = set() + for index in range(len(parts)): + suffix = ".".join(parts[index:]) + if suffix: + names.add(suffix) + return names + + +def _module_resolutions( + module_paths: list[str], + literal_requires: list[LiteralRequireRelation], +) -> tuple[list[ModuleResolutionRelation], list[AnalysisBoundary]]: + resolutions: list[ModuleResolutionRelation] = [] + boundaries: list[AnalysisBoundary] = [] + for require in literal_requires: + candidates = sorted( + { + module_path + for module_path in module_paths + if module_path != require.caller_module_path + and require.require_string in _module_names_for_path(module_path) + } + ) + if len(candidates) == 1: + status = "matched" + target_module_path = candidates[0] + reason = "" + elif candidates: + status = "ambiguous" + target_module_path = "" + reason = "ambiguous-module-path-candidates" + else: + status = "unresolved" + target_module_path = "" + reason = "no-module-path-candidate" + resolutions.append( + ModuleResolutionRelation( + caller_module_path=require.caller_module_path, + callsite_id=require.callsite_id, + require_string=require.require_string, + status=status, + target_module_path=target_module_path, + reason=reason, + provenance="bytecode-only,literal-require,module-path", + ) + ) + if status != "matched": + boundaries.append( + AnalysisBoundary( + module_path=require.caller_module_path, + prototype_id=require.caller_prototype_id, + site_id=require.callsite_id, + boundary_kind="unresolved-module-require", + reason=reason, + provenance="bytecode-only,module-resolution-boundary", + ) + ) + return resolutions, boundaries + + +def _cross_module_field_call_resolutions( + chunks_by_module: dict[str, Chunk], + instructions: list[InstructionIdentityRelation], + flows: list[ValueFlowRelation], + upvalue_flows: list[UpvalueFlowRelation], + existing_resolutions: list[CallResolutionRelation], + literal_requires: list[LiteralRequireRelation], + module_resolutions: list[ModuleResolutionRelation], + module_exports: list[ModuleExportRelation], +) -> tuple[ + list[CallResolutionRelation], + set[tuple[str, str, str]], + set[tuple[str, str, str]], +]: + chunks_by_module_prototype = { + (module_path, prototype_id): chunk + for module_path, root_chunk in chunks_by_module.items() + for prototype_id, chunk in _chunks_by_prototype(root_chunk).items() + } + instructions_by_module_prototype: dict[ + tuple[str, str], list[InstructionIdentityRelation] + ] = defaultdict(list) + instruction_by_site: dict[ + tuple[str, str, int], InstructionIdentityRelation + ] = {} + for instruction in instructions: + key = (instruction.module_path, instruction.prototype_id) + instructions_by_module_prototype[key].append(instruction) + instruction_by_site[(key[0], key[1], instruction.pc)] = instruction + + reaching_sources: dict[tuple[str, str, str], set[str]] = defaultdict(set) + carrier_adjacency: dict[tuple[str, str], set[str]] = defaultdict(set) + for flow in flows: + if flow.kind == "reaching-definition": + reaching_sources[ + (flow.module_path, flow.prototype_id, flow.sink_ref) + ].add(flow.source_ref) + carrier_adjacency[(flow.module_path, flow.source_ref)].add(flow.sink_ref) + for flow in upvalue_flows: + carrier_adjacency[(flow.module_path, flow.capture_ref)].add(flow.read_ref) + + requires_by_site = { + (require.caller_module_path, require.callsite_id): require + for require in literal_requires + } + exports_by_module_field: dict[ + tuple[str, str], list[ModuleExportRelation] + ] = defaultdict(list) + export_path_prefixes_by_module: dict[str, set[tuple[str, ...]]] = ( + defaultdict(set) + ) + for export in module_exports: + exports_by_module_field[(export.module_path, export.field_name)].append(export) + field_path = tuple(export.field_name.split(".")) + for prefix_length in range(1, len(field_path) + 1): + export_path_prefixes_by_module[export.module_path].add( + field_path[:prefix_length] + ) + occupied_callsites = { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + ) + for resolution in existing_resolutions + if resolution.target_module_path or resolution.target_prototype_id + } + + derived: list[CallResolutionRelation] = [] + resolved_sites: set[tuple[str, str, str]] = set() + unresolved_sites: set[tuple[str, str, str]] = set() + for module_resolution in module_resolutions: + if module_resolution.status != "matched": + continue + require = requires_by_site.get( + (module_resolution.caller_module_path, module_resolution.callsite_id) + ) + if require is None: + continue + require_pc = int(require.callsite_id.rsplit("@pc", 1)[1]) + require_call = instruction_by_site.get( + ( + require.caller_module_path, + require.caller_prototype_id, + require_pc, + ) + ) + if ( + require_call is None + or require_call.opcode != "CALL" + or require_call.operand_c != 2 + ): + continue + require_result_ref = ( + f"{require.caller_prototype_id}@pc{require_pc}:r{require_call.operand_a}" + ) + candidate_scopes = sorted( + scope + for scope in instructions_by_module_prototype + if scope[0] == require.caller_module_path + ) + static_fields: list[ + tuple[ + tuple[str, str], + str, + str, + str, + ] + ] = [] + for scope in candidate_scopes: + chunk = chunks_by_module_prototype[scope] + scoped_instructions = instructions_by_module_prototype[scope] + for field_instruction in scoped_instructions: + if field_instruction.opcode != "GETTABLE": + continue + field_name = _constant_string_operand( + chunk, + field_instruction.operand_c, + ) + if field_name is None: + continue + field_base_ref = ( + f"{scope[1]}@pc{field_instruction.pc}:" + f"r{field_instruction.operand_b}" + ) + field_result_ref = ( + f"{scope[1]}@pc{field_instruction.pc}:" + f"r{field_instruction.operand_a}" + ) + static_fields.append( + ( + scope, + field_name, + field_base_ref, + field_result_ref, + ) + ) + + static_paths_by_ref: dict[str, set[tuple[str, ...]]] = defaultdict(set) + field_result_paths_by_ref: dict[str, set[tuple[str, ...]]] = { + field_result_ref: set() + for _, _, _, field_result_ref in static_fields + } + export_path_prefixes = export_path_prefixes_by_module.get( + module_resolution.target_module_path, + set(), + ) + pending_paths: deque[tuple[str, tuple[str, ...]]] = deque( + [(require_result_ref, ())] + ) + while True: + while pending_paths: + source_ref, field_path = pending_paths.popleft() + if field_path in static_paths_by_ref[source_ref]: + continue + static_paths_by_ref[source_ref].add(field_path) + for sink_ref in carrier_adjacency.get( + (require.caller_module_path, source_ref), set() + ): + pending_paths.append((sink_ref, field_path)) + + added_field_path = False + for ( + scope, + field_name, + field_base_ref, + field_result_ref, + ) in static_fields: + field_base_sources = reaching_sources.get( + (scope[0], scope[1], field_base_ref), set() + ) + if not field_base_sources or not all( + field_result_paths_by_ref.get( + source_ref, + static_paths_by_ref.get(source_ref, set()), + ) + for source_ref in field_base_sources + ): + continue + for source_ref in sorted(field_base_sources): + source_paths = field_result_paths_by_ref.get( + source_ref, + static_paths_by_ref.get(source_ref, set()), + ) + for field_path in sorted(source_paths): + nested_path = (*field_path, field_name) + if ( + len(nested_path) > 1 + and nested_path not in export_path_prefixes + ): + continue + if ( + nested_path + in field_result_paths_by_ref[field_result_ref] + ): + continue + field_result_paths_by_ref[field_result_ref].add(nested_path) + pending_paths.append((field_result_ref, nested_path)) + added_field_path = True + if not added_field_path: + break + + for scope in candidate_scopes: + for call in instructions_by_module_prototype[scope]: + if call.opcode not in {"CALL", "TAILCALL"}: + continue + callsite_id = f"{scope[1]}@pc{call.pc}" + site = (scope[0], scope[1], callsite_id) + if site in occupied_callsites or site in resolved_sites: + continue + target_ref = f"{scope[1]}@pc{call.pc}:r{call.operand_a}" + target_sources = reaching_sources.get( + (scope[0], scope[1], target_ref), set() + ) + if len(target_sources) != 1: + continue + target_source = next(iter(target_sources)) + field_paths = { + field_path + for field_path in field_result_paths_by_ref.get( + target_source, + set(), + ) + if field_path + } + if not field_paths: + continue + site_resolutions: list[CallResolutionRelation] = [] + for field_path in sorted(field_paths): + joined_field_path = ".".join(field_path) + exports = exports_by_module_field.get( + ( + module_resolution.target_module_path, + joined_field_path, + ), + [], + ) + target_prototypes = { + export.target_prototype_id for export in exports + } + for target_prototype_id in sorted(target_prototypes): + export = min( + ( + export + for export in exports + if export.target_prototype_id == target_prototype_id + ), + key=lambda item: (item.provenance, item.value_ref), + ) + provenance_suffix = export.provenance.split(",", 1)[1] + site_resolutions.append( + CallResolutionRelation( + caller_module_path=scope[0], + caller_prototype_id=scope[1], + callsite_id=callsite_id, + target_value_ref=target_ref, + resolved_name=( + f"{require.require_string}." + f"{joined_field_path}" + ), + resolution_kind="module-field-export", + target_module_path=( + module_resolution.target_module_path + ), + target_prototype_id=target_prototype_id, + provenance=( + "bytecode-only,literal-require," + f"{provenance_suffix}" + ), + ) + ) + if site_resolutions: + derived.extend(site_resolutions) + resolved_sites.add(site) + else: + unresolved_sites.add(site) + return derived, resolved_sites, unresolved_sites + + +def _same_module_export_call_resolutions( + call_resolutions: list[CallResolutionRelation], + module_exports: list[ModuleExportRelation], +) -> list[CallResolutionRelation]: + exports_by_module_name: dict[ + tuple[str, str], list[ModuleExportRelation] + ] = defaultdict(list) + for export in module_exports: + if export.export_kind in {"module-global", "module-global-table-field"}: + exports_by_module_name[(export.module_path, export.field_name)].append( + export + ) + + upgraded: list[CallResolutionRelation] = [] + for resolution in call_resolutions: + if resolution.target_module_path or resolution.target_prototype_id: + upgraded.append(resolution) + continue + exports = exports_by_module_name.get( + (resolution.caller_module_path, resolution.resolved_name), + [], + ) + target_prototypes = {export.target_prototype_id for export in exports} + if len(target_prototypes) != 1: + upgraded.append(resolution) + continue + target_prototype_id = next(iter(target_prototypes)) + export = min( + ( + export + for export in exports + if export.target_prototype_id == target_prototype_id + ), + key=lambda item: (item.provenance, item.value_ref), + ) + provenance_suffix = export.provenance.split(",", 1)[1] + upgraded.append( + CallResolutionRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + target_value_ref=resolution.target_value_ref, + resolved_name=resolution.resolved_name, + resolution_kind="same-module-field-export", + target_module_path=resolution.caller_module_path, + target_prototype_id=target_prototype_id, + provenance=f"bytecode-only,same-module,{provenance_suffix}", + ) + ) + return upgraded + + +def _interprocedural_argument_flows( + prototypes: list[PrototypeIdentityRelation], + instructions: list[InstructionIdentityRelation], + value_flows: list[ValueFlowRelation], + call_resolutions: list[CallResolutionRelation], +) -> list[InterproceduralFlowRelation]: + prototype_by_scope = { + (prototype.module_path, prototype.prototype_id): prototype + for prototype in prototypes + } + instruction_by_site = { + (instruction.module_path, instruction.prototype_id, instruction.pc): instruction + for instruction in instructions + } + fixed_varargs_by_scope: dict[ + tuple[str, str], list[InstructionIdentityRelation] + ] = defaultdict(list) + for instruction in instructions: + if instruction.opcode == "VARARG" and instruction.operand_b > 0: + fixed_varargs_by_scope[ + (instruction.module_path, instruction.prototype_id) + ].append(instruction) + proven_reads_by_site: dict[ + tuple[str, str, str], dict[int, str] + ] = defaultdict(dict) + for flow in value_flows: + if flow.kind != "reaching-definition": + continue + site_id, separator, slot_text = flow.sink_ref.rpartition(":r") + if not separator or not slot_text.isdigit(): + continue + proven_reads_by_site[ + (flow.module_path, flow.prototype_id, site_id) + ][int(slot_text)] = flow.sink_ref + flows: list[InterproceduralFlowRelation] = [] + for resolution in call_resolutions: + callee = prototype_by_scope.get( + (resolution.target_module_path, resolution.target_prototype_id) + ) + if callee is None: + continue + call_pc = int(resolution.callsite_id.rsplit("@pc", 1)[1]) + call = instruction_by_site.get( + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + call_pc, + ) + ) + if call is None or call.opcode not in {"CALL", "TAILCALL"}: + continue + if call.opcode == "TAILCALL" and call.operand_b == 0: + continue + resolution_provenance = ( + "resolved-tailcall" if call.opcode == "TAILCALL" else "resolved-call" + ) + if call.operand_b == 0: + for slot, source_ref in sorted( + proven_reads_by_site.get( + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + ), + {}, + ).items() + ): + if slot <= call.operand_a: + continue + position = slot - call.operand_a - 1 + if position < callee.num_params: + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=source_ref, + sink_ref=f"{resolution.target_prototype_id}:r{position}", + flow_kind="argument-to-parameter", + position=position, + provenance=( + "bytecode-only,resolved-call," + "producer-proven-open-argument" + ), + ) + ) + continue + if not callee.is_vararg: + continue + vararg_offset = position - callee.num_params + for vararg in fixed_varargs_by_scope.get( + ( + resolution.target_module_path, + resolution.target_prototype_id, + ), + [], + ): + if vararg_offset >= vararg.operand_b - 1: + continue + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=source_ref, + sink_ref=( + f"{resolution.target_prototype_id}@pc{vararg.pc}:" + f"r{vararg.operand_a + vararg_offset}" + ), + flow_kind="argument-to-vararg", + position=position, + provenance=( + "bytecode-only,resolved-call," + "producer-proven-open-vararg" + ), + ) + ) + continue + argument_count = call.operand_b - 1 + for position in range(min(argument_count, callee.num_params)): + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a + 1 + position}" + ), + sink_ref=f"{resolution.target_prototype_id}:r{position}", + flow_kind="argument-to-parameter", + position=position, + provenance=( + f"bytecode-only,{resolution_provenance},fixed-argument" + ), + ) + ) + if call.opcode == "TAILCALL": + continue + if not callee.is_vararg: + continue + extra_argument_count = argument_count - callee.num_params + if extra_argument_count <= 0: + continue + for vararg in fixed_varargs_by_scope.get( + (resolution.target_module_path, resolution.target_prototype_id), + [], + ): + for offset in range(min(extra_argument_count, vararg.operand_b - 1)): + position = callee.num_params + offset + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a + 1 + position}" + ), + sink_ref=( + f"{resolution.target_prototype_id}@pc{vararg.pc}:" + f"r{vararg.operand_a + offset}" + ), + flow_kind="argument-to-vararg", + position=position, + provenance="bytecode-only,resolved-call,fixed-vararg", + ) + ) + return sorted( + flows, + key=lambda flow: ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.position, + flow.source_ref, + flow.sink_ref, + ), + ) + + +def _interprocedural_return_flows( + instructions: list[InstructionIdentityRelation], + value_flows: list[ValueFlowRelation], + call_resolutions: list[CallResolutionRelation], +) -> list[InterproceduralFlowRelation]: + instruction_by_site = { + (instruction.module_path, instruction.prototype_id, instruction.pc): instruction + for instruction in instructions + } + returns_by_scope: dict[ + tuple[str, str], list[InstructionIdentityRelation] + ] = defaultdict(list) + structural_tailcalls_by_scope: dict[ + tuple[str, str], list[InstructionIdentityRelation] + ] = defaultdict(list) + for instruction in instructions: + if instruction.opcode == "RETURN" and instruction.operand_b != 1: + returns_by_scope[ + (instruction.module_path, instruction.prototype_id) + ].append(instruction) + elif instruction.opcode == "TAILCALL" and instruction.operand_c != 1: + structural_tailcalls_by_scope[ + (instruction.module_path, instruction.prototype_id) + ].append(instruction) + proven_reads_by_site: dict[ + tuple[str, str, str], dict[int, str] + ] = defaultdict(dict) + for flow in value_flows: + if flow.kind != "reaching-definition": + continue + site_id, separator, slot_text = flow.sink_ref.rpartition(":r") + if not separator or not slot_text.isdigit(): + continue + proven_reads_by_site[ + (flow.module_path, flow.prototype_id, site_id) + ][int(slot_text)] = flow.sink_ref + resolved_callsites = { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + ) + for resolution in call_resolutions + if resolution.target_module_path and resolution.target_prototype_id + } + + flows: list[InterproceduralFlowRelation] = [] + for resolution in call_resolutions: + call_pc = int(resolution.callsite_id.rsplit("@pc", 1)[1]) + call = instruction_by_site.get( + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + call_pc, + ) + ) + if call is None or call.opcode not in {"CALL", "TAILCALL"}: + continue + if call.opcode == "CALL" and call.operand_c == 1: + continue + if call.opcode == "TAILCALL" and call.operand_c != 0: + continue + resolution_provenance = ( + "resolved-tailcall" if call.opcode == "TAILCALL" else "resolved-call" + ) + for return_instruction in returns_by_scope.get( + (resolution.target_module_path, resolution.target_prototype_id), + [], + ): + if return_instruction.operand_b == 0: + if call.opcode == "TAILCALL": + continue + return_site_id = ( + f"{resolution.target_prototype_id}@pc{return_instruction.pc}" + ) + for slot, source_ref in sorted( + proven_reads_by_site.get( + ( + resolution.target_module_path, + resolution.target_prototype_id, + return_site_id, + ), + {}, + ).items() + ): + if slot < return_instruction.operand_a: + continue + position = slot - return_instruction.operand_a + if call.operand_c > 0 and position >= call.operand_c - 1: + continue + provenance = ( + "bytecode-only,resolved-call," + "producer-proven-open-return" + ) + if call.operand_c == 0: + provenance += ",producer-proven-open-result" + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=source_ref, + sink_ref=( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a + position}" + ), + flow_kind="return-to-result", + position=position, + provenance=provenance, + ) + ) + continue + if call.operand_c == 0: + position_count = return_instruction.operand_b - 1 + provenance = ( + f"bytecode-only,{resolution_provenance}," + "producer-proven-open-result" + ) + else: + position_count = min( + call.operand_c - 1, + return_instruction.operand_b - 1, + ) + provenance = "bytecode-only,resolved-call,fixed-return" + for position in range(position_count): + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=( + f"{resolution.target_prototype_id}@pc" + f"{return_instruction.pc}:" + f"r{return_instruction.operand_a + position}" + ), + sink_ref=( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a + position}" + ), + flow_kind="return-to-result", + position=position, + provenance=provenance, + ) + ) + if call.opcode != "CALL": + continue + for tailcall in structural_tailcalls_by_scope.get( + (resolution.target_module_path, resolution.target_prototype_id), + [], + ): + tailcall_site_id = ( + f"{resolution.target_prototype_id}@pc{tailcall.pc}" + ) + if ( + resolution.target_module_path, + resolution.target_prototype_id, + tailcall_site_id, + ) in resolved_callsites: + continue + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=( + f"{resolution.target_prototype_id}@pc{tailcall.pc}:" + f"r{tailcall.operand_a}" + ), + sink_ref=( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a}" + ), + flow_kind="return-to-result", + position=0, + provenance=( + f"bytecode-only,{resolution_provenance}," + "structural-tailcall-result" + ), + ) + ) + if tailcall.operand_b == 0: + argument_refs = [ + ( + source_ref, + ( + "bytecode-only,resolved-call," + "structural-tailcall-producer-proven-open-argument" + ), + ) + for slot, source_ref in sorted( + proven_reads_by_site.get( + ( + resolution.target_module_path, + resolution.target_prototype_id, + tailcall_site_id, + ), + {}, + ).items() + ) + if slot > tailcall.operand_a + ] + else: + argument_refs = [ + ( + ( + f"{resolution.target_prototype_id}@pc{tailcall.pc}:" + f"r{argument_slot}" + ), + ( + "bytecode-only,resolved-call," + "structural-tailcall-argument" + ), + ) + for argument_slot in range( + tailcall.operand_a + 1, + tailcall.operand_a + tailcall.operand_b, + ) + ] + for source_ref, argument_provenance in argument_refs: + flows.append( + InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=source_ref, + sink_ref=( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a}" + ), + flow_kind="return-to-result", + position=0, + provenance=argument_provenance, + ) + ) + + callers_by_target_scope: dict[ + tuple[str, str], + list[tuple[CallResolutionRelation, InstructionIdentityRelation]], + ] = defaultdict(list) + for resolution in call_resolutions: + call_pc = int(resolution.callsite_id.rsplit("@pc", 1)[1]) + call = instruction_by_site.get( + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + call_pc, + ) + ) + if call is None or call.opcode not in {"CALL", "TAILCALL"}: + continue + if call.opcode == "CALL" and call.operand_c == 1: + continue + if call.opcode == "TAILCALL" and call.operand_c != 0: + continue + callers_by_target_scope[ + (resolution.target_module_path, resolution.target_prototype_id) + ].append((resolution, call)) + + tailcall_summaries = deque( + flow + for flow in flows + if flow.provenance.startswith("bytecode-only,resolved-tailcall,") + ) + forwarded: set[tuple[str, str, str, str, int]] = set() + while tailcall_summaries: + tailcall_return = tailcall_summaries.popleft() + target_scope = ( + tailcall_return.caller_module_path, + tailcall_return.caller_prototype_id, + ) + for resolution, call in callers_by_target_scope.get(target_scope, []): + position = tailcall_return.position + if call.operand_c > 0 and position >= call.operand_c - 1: + continue + call_pc = int(resolution.callsite_id.rsplit("@pc", 1)[1]) + sink_ref = ( + f"{resolution.caller_prototype_id}@pc{call_pc}:" + f"r{call.operand_a + position}" + ) + key = ( + resolution.caller_module_path, + resolution.callsite_id, + tailcall_return.sink_ref, + sink_ref, + position, + ) + if key in forwarded: + continue + forwarded.add(key) + if call.opcode == "TAILCALL": + provenance = ( + "bytecode-only,resolved-tailcall,tailcall-chain-forward" + ) + else: + provenance = ( + "bytecode-only,resolved-call,resolved-tailcall," + "tailcall-forward" + ) + forwarded_flow = InterproceduralFlowRelation( + caller_module_path=resolution.caller_module_path, + caller_prototype_id=resolution.caller_prototype_id, + callsite_id=resolution.callsite_id, + callee_module_path=resolution.target_module_path, + callee_prototype_id=resolution.target_prototype_id, + source_ref=tailcall_return.sink_ref, + sink_ref=sink_ref, + flow_kind="return-to-result", + position=position, + provenance=provenance, + ) + flows.append(forwarded_flow) + if call.opcode == "TAILCALL": + tailcall_summaries.append(forwarded_flow) + return flows + + +def _root_module_call_evidence( + module_path: str, + chunk: Chunk, + flows: list[ValueFlowRelation], + call_resolutions: list[CallResolutionRelation], +) -> tuple[str | None, bool]: + reaching_sources_by_ref: dict[str, set[str]] = defaultdict(set) + for flow in flows: + if flow.prototype_id == "root" and flow.kind == "reaching-definition": + reaching_sources_by_ref[flow.sink_ref].add(flow.source_ref) + instruction_by_ref = { + f"root@pc{pc}:r{instruction.a}": (pc, instruction) + for pc, instruction in enumerate(chunk.instructions) + } + module_names: set[str] = set() + seeall_module_names: set[str] = set() + for resolution in call_resolutions: + if ( + resolution.caller_prototype_id != "root" + or resolution.resolved_name != "module" + ): + continue + pc = int(resolution.callsite_id.rsplit("@pc", 1)[1]) + call = chunk.instructions[pc] + argument_ref = f"root@pc{pc}:r{call.a + 1}" + sources = reaching_sources_by_ref.get(argument_ref, set()) + if len(sources) != 1: + continue + loadk_entry = instruction_by_ref.get(next(iter(sources))) + if loadk_entry is None or loadk_entry[1].opcode.name != "LOADK": + continue + loadk = loadk_entry[1] + module_name = _constant_string(chunk, loadk.b) + if module_name not in _module_names_for_path(module_path): + continue + module_names.add(module_name) + if call.b < 3: + continue + seeall_ref = f"root@pc{pc}:r{call.a + 2}" + seeall_names: set[str] = set() + for member_ref in reaching_sources_by_ref.get(seeall_ref, set()): + member_entry = instruction_by_ref.get(member_ref) + if member_entry is None or member_entry[1].opcode.name != "GETTABLE": + continue + member_pc, member = member_entry + member_name = _constant_string_operand(chunk, member.c) + if member_name is None: + continue + base_ref = f"root@pc{member_pc}:r{member.b}" + for global_ref in reaching_sources_by_ref.get(base_ref, set()): + global_entry = instruction_by_ref.get(global_ref) + if global_entry is None or global_entry[1].opcode.name != "GETGLOBAL": + continue + global_name = _constant_string(chunk, global_entry[1].b) + if global_name is not None: + seeall_names.add(f"{global_name}.{member_name}") + if seeall_names == {"package.seeall"}: + seeall_module_names.add(module_name) + if len(module_names) != 1: + return None, False + module_name = next(iter(module_names)) + return module_name, module_name in seeall_module_names + + +def _root_returned_table_exports( + module_path: str, + chunk: Chunk, + flows: list[ValueFlowRelation], + call_resolutions: list[CallResolutionRelation], +) -> list[ModuleExportRelation]: + module_call_name, module_seeall = _root_module_call_evidence( + module_path, + chunk, + flows, + call_resolutions, + ) + table_by_slot: dict[int, str] = {} + closure_by_slot: dict[int, tuple[str, str]] = {} + global_closure_by_name: dict[str, tuple[str, str]] = {} + aliased_closure_slots: set[int] = set() + fields_by_table: dict[str, dict[str, tuple[str, str]]] = defaultdict(dict) + global_table_by_name: dict[str, str] = {} + global_names_by_table: dict[str, set[str]] = defaultdict(set) + returned_tables: set[str] = set() + returned_closures: set[tuple[str, str]] = set() + global_exports: list[ModuleExportRelation] = [] + closure_binding_pcs = _closure_binding_pcs(chunk) + + def clear_slot(slot: int) -> None: + table_by_slot.pop(slot, None) + closure_by_slot.pop(slot, None) + aliased_closure_slots.discard(slot) + + for pc, instruction in enumerate(chunk.instructions): + if pc in closure_binding_pcs: + continue + opcode = instruction.opcode.name + if opcode == "NEWTABLE": + clear_slot(instruction.a) + table_ref = f"root@pc{pc}:r{instruction.a}" + table_by_slot[instruction.a] = table_ref + fields_by_table.setdefault(table_ref, {}) + elif opcode == "CLOSURE": + clear_slot(instruction.a) + if instruction.b < len(chunk.protos): + closure_by_slot[instruction.a] = ( + f"root@pc{pc}:r{instruction.a}", + f"root.{instruction.b}", + ) + elif opcode == "MOVE": + table_ref = table_by_slot.get(instruction.b) + closure = closure_by_slot.get(instruction.b) + closure_is_alias = instruction.b in aliased_closure_slots + clear_slot(instruction.a) + if table_ref is not None: + table_by_slot[instruction.a] = table_ref + if closure is not None: + closure_by_slot[instruction.a] = closure + if closure_is_alias: + aliased_closure_slots.add(instruction.a) + elif opcode == "GETGLOBAL": + clear_slot(instruction.a) + global_name = _constant_string(chunk, instruction.b) + table_ref = ( + global_table_by_name.get(global_name) + if global_name is not None + else None + ) + if table_ref is not None: + table_by_slot[instruction.a] = table_ref + closure = ( + global_closure_by_name.get(global_name) + if module_seeall and global_name is not None + else None + ) + if closure is not None: + closure_by_slot[instruction.a] = closure + aliased_closure_slots.add(instruction.a) + elif opcode == "SETTABLE": + table_ref = table_by_slot.get(instruction.a) + field_name = _constant_string_operand(chunk, instruction.b) + closure = ( + closure_by_slot.get(instruction.c) + if instruction.c < LUA_RK_CONSTANT_BIT + else None + ) + if table_ref is not None and field_name is not None and closure is not None: + fields_by_table[table_ref][field_name] = closure + if module_call_name is not None: + for global_name in sorted(global_names_by_table[table_ref]): + global_exports.append( + ModuleExportRelation( + module_path=module_path, + export_kind="module-global-table-field", + field_name=f"{global_name}.{field_name}", + value_ref=closure[0], + target_prototype_id=closure[1], + provenance=( + "bytecode-only,module-global-table-field-export," + "module-call" + ), + ) + ) + if module_seeall: + global_exports.append( + ModuleExportRelation( + module_path=module_path, + export_kind="module-global-table-field", + field_name=f"{global_name}.{field_name}", + value_ref=closure[0], + target_prototype_id=closure[1], + provenance=( + "bytecode-only,module-global-table-field-export," + "module-seeall" + ), + ) + ) + elif opcode == "SETGLOBAL": + closure = closure_by_slot.get(instruction.a) + table_ref = table_by_slot.get(instruction.a) + field_name = _constant_string(chunk, instruction.b) + if module_call_name is not None and field_name: + previous_table_ref = global_table_by_name.pop(field_name, None) + if previous_table_ref is not None: + global_names_by_table[previous_table_ref].discard(field_name) + if table_ref is not None: + global_table_by_name[field_name] = table_ref + global_names_by_table[table_ref].add(field_name) + if module_call_name is not None and closure is not None and field_name: + if module_seeall: + global_closure_by_name[field_name] = closure + if instruction.a not in aliased_closure_slots: + global_exports.append( + ModuleExportRelation( + module_path=module_path, + export_kind="module-global", + field_name=field_name, + value_ref=closure[0], + target_prototype_id=closure[1], + provenance="bytecode-only,module-global-export,module-call", + ) + ) + if module_seeall: + global_exports.append( + ModuleExportRelation( + module_path=module_path, + export_kind="module-global", + field_name=field_name, + value_ref=closure[0], + target_prototype_id=closure[1], + provenance="bytecode-only,module-global-export,module-seeall", + ) + ) + elif opcode == "RETURN" and instruction.b == 2: + table_ref = table_by_slot.get(instruction.a) + if table_ref is not None: + returned_tables.add(table_ref) + closure = closure_by_slot.get(instruction.a) + if closure is not None: + returned_closures.add(closure) + else: + effects = _fixed_instruction_effects( + opcode, + instruction.a, + instruction.b, + instruction.c, + ) + if effects is not None: + for slot in effects[1]: + clear_slot(slot) + + exports = list(global_exports) + for table_ref in sorted(returned_tables): + for field_name, (value_ref, target_prototype_id) in sorted( + fields_by_table[table_ref].items() + ): + exports.append( + ModuleExportRelation( + module_path=module_path, + export_kind="returned-table-field", + field_name=field_name, + value_ref=value_ref, + target_prototype_id=target_prototype_id, + provenance="bytecode-only,module-return-table", + ) + ) + for value_ref, target_prototype_id in sorted(returned_closures): + exports.append( + ModuleExportRelation( + module_path=module_path, + export_kind="returned-closure", + field_name="", + value_ref=value_ref, + target_prototype_id=target_prototype_id, + provenance="bytecode-only,module-return-closure", + ) + ) + return exports + + +def analyze_corpus(corpus: AcceptedCorpus) -> AnalysisResult: + artifact_identities: list[ArtifactIdentityRelation] = [] + prototype_identities: list[PrototypeIdentityRelation] = [] + instruction_identities: list[InstructionIdentityRelation] = [] + value_identities: list[ValueIdentityRelation] = [] + value_flows: list[ValueFlowRelation] = [] + table_field_flows: list[TableFieldFlowRelation] = [] + global_flows: list[GlobalFlowRelation] = [] + upvalue_flows: list[UpvalueFlowRelation] = [] + call_resolutions: list[CallResolutionRelation] = [] + literal_requires: list[LiteralRequireRelation] = [] + module_exports: list[ModuleExportRelation] = [] + boundaries: list[AnalysisBoundary] = [] + chunks_by_module: dict[str, Chunk] = {} + seen_module_paths: set[str] = set() + + for artifact in sorted(corpus.artifacts, key=lambda item: item.module_path): + _validate_module_path(artifact.module_path) + if artifact.module_path in seen_module_paths: + raise ValueError(f"duplicate normalized module path: {artifact.module_path}") + seen_module_paths.add(artifact.module_path) + + loaded = artifact.loaded_artifact + if not loaded.accepted or loaded.chunk is None: + raise ValueError(f"corpus artifact is not accepted bytecode: {artifact.module_path}") + chunks_by_module[artifact.module_path] = loaded.chunk + + artifact_identities.append( + ArtifactIdentityRelation( + module_path=artifact.module_path, + profile_id=loaded.profile_id, + ) + ) + ( + prototypes, + instructions, + values, + flows, + table_flows, + prototype_global_flows, + prototype_upvalue_flows, + prototype_boundaries, + ) = _walk_prototypes( + artifact.module_path, + loaded.chunk, + "root", + "", + -1, + ) + prototype_call_resolutions, call_boundaries = _artifact_call_resolutions( + artifact.module_path, + loaded.chunk, + prototypes, + instructions, + flows, + table_flows, + prototype_global_flows, + prototype_upvalue_flows, + ) + prototype_literal_requires, literal_require_boundaries = ( + _artifact_literal_requires( + artifact.module_path, + loaded.chunk, + instructions, + flows, + prototype_call_resolutions, + ) + ) + prototype_identities.extend(prototypes) + instruction_identities.extend(instructions) + value_identities.extend(values) + value_flows.extend(flows) + table_field_flows.extend(table_flows) + global_flows.extend(prototype_global_flows) + upvalue_flows.extend(prototype_upvalue_flows) + call_resolutions.extend(prototype_call_resolutions) + literal_requires.extend(prototype_literal_requires) + module_exports.extend( + _root_returned_table_exports( + artifact.module_path, + loaded.chunk, + flows, + prototype_call_resolutions, + ) + ) + boundaries.extend(prototype_boundaries) + boundaries.extend(call_boundaries) + boundaries.extend(literal_require_boundaries) + + module_resolutions, module_resolution_boundaries = _module_resolutions( + [identity.module_path for identity in artifact_identities], + literal_requires, + ) + boundaries.extend(module_resolution_boundaries) + ( + module_field_resolutions, + resolved_module_field_sites, + unresolved_module_field_sites, + ) = ( + _cross_module_field_call_resolutions( + chunks_by_module, + instruction_identities, + value_flows, + upvalue_flows, + call_resolutions, + literal_requires, + module_resolutions, + module_exports, + ) + ) + reconciled_module_field_sites = ( + resolved_module_field_sites | unresolved_module_field_sites + ) + call_resolutions = [ + resolution + for resolution in call_resolutions + if ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + ) not in reconciled_module_field_sites + ] + call_resolutions.extend(module_field_resolutions) + call_resolutions = _same_module_export_call_resolutions( + call_resolutions, + module_exports, + ) + interprocedural_flows = _interprocedural_argument_flows( + prototype_identities, + instruction_identities, + value_flows, + call_resolutions, + ) + interprocedural_flows.extend( + _interprocedural_return_flows( + instruction_identities, + value_flows, + call_resolutions, + ) + ) + interprocedural_flows.sort( + key=lambda flow: ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.flow_kind, + flow.position, + flow.source_ref, + flow.sink_ref, + ) + ) + boundaries = [ + boundary + for boundary in boundaries + if not ( + boundary.boundary_kind == "unresolved-call-target" + and (boundary.module_path, boundary.prototype_id, boundary.site_id) + in resolved_module_field_sites + ) + ] + unresolved_boundary_sites = { + (boundary.module_path, boundary.prototype_id, boundary.site_id) + for boundary in boundaries + if boundary.boundary_kind == "unresolved-call-target" + } + boundaries.extend( + AnalysisBoundary( + module_path=module_path, + prototype_id=prototype_id, + site_id=callsite_id, + boundary_kind="unresolved-call-target", + reason="no-proven-target", + provenance="bytecode-only,call-resolution-boundary", + ) + for module_path, prototype_id, callsite_id in sorted( + unresolved_module_field_sites + ) + if (module_path, prototype_id, callsite_id) not in unresolved_boundary_sites + ) + control_flow_edges = _control_flow_edges(instruction_identities) + dominator_tree_intervals = _dominator_tree_intervals(instruction_identities) + return AnalysisResult( + artifact_identities=tuple(artifact_identities), + prototype_identities=tuple(prototype_identities), + instruction_identities=tuple(instruction_identities), + value_identities=tuple(value_identities), + value_flows=tuple(value_flows), + control_flow_edges=tuple(control_flow_edges), + dominator_tree_intervals=tuple(dominator_tree_intervals), + table_field_flows=tuple(table_field_flows), + global_flows=tuple(global_flows), + upvalue_flows=tuple(upvalue_flows), + call_resolutions=tuple(call_resolutions), + literal_requires=tuple(literal_requires), + module_resolutions=tuple(module_resolutions), + module_exports=tuple(module_exports), + interprocedural_flows=tuple(interprocedural_flows), + boundaries=tuple(boundaries), + ) diff --git a/lua/tools/index-files.sh b/lua/tools/index-files.sh new file mode 100755 index 000000000000..f3ba407addce --- /dev/null +++ b/lua/tools/index-files.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +exec python3 "$CODEQL_EXTRACTOR_LUA_ROOT/tools/index_lua_files.py" \ + --file-list "$1" \ + --source-archive-dir "$CODEQL_EXTRACTOR_LUA_SOURCE_ARCHIVE_DIR" \ + --output-dir "$CODEQL_EXTRACTOR_LUA_TRAP_DIR" diff --git a/lua/tools/index.sh b/lua/tools/index.sh new file mode 100755 index 000000000000..b3c82292d260 --- /dev/null +++ b/lua/tools/index.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +exec python3 "$CODEQL_EXTRACTOR_LUA_ROOT/tools/index_lua_files.py" \ + --source-root "$(pwd)" \ + --source-archive-dir "$CODEQL_EXTRACTOR_LUA_SOURCE_ARCHIVE_DIR" \ + --output-dir "$CODEQL_EXTRACTOR_LUA_TRAP_DIR" diff --git a/lua/tools/index_lua_files.py b/lua/tools/index_lua_files.py new file mode 100755 index 000000000000..172ccb76a10a --- /dev/null +++ b/lua/tools/index_lua_files.py @@ -0,0 +1,862 @@ +#!/usr/bin/env python3 +"""Emit CodeQL TRAP facts for Lua source and Lua 5.1 bytecode smoke tests.""" + +from __future__ import annotations + +import argparse +import hashlib +import shutil +from pathlib import Path + +from corpus_analyzer import ( + AcceptedCorpus, + AnalysisBoundary, + ArtifactIdentityRelation, + CallResolutionRelation, + ControlFlowEdgeRelation, + CorpusArtifact, + DominatorTreeIntervalRelation, + GlobalFlowRelation, + InterproceduralFlowRelation, + LiteralRequireRelation, + ModuleExportRelation, + ModuleResolutionRelation, + TableFieldFlowRelation, + UpvalueFlowRelation, + ValueFlowRelation, + analyze_corpus, +) +from lua_bytecode import ( + BytecodeError, + Chunk, + Instruction, + LoadedArtifact, + Lua51Loader, +) +from semantic_normalizer import ( + CallSite, + ClosureValue, + InstructionSemanticNormalizer, + RegisterEvent, + SemanticStep, +) +from trap_writer import TrapLabel, TrapWriter + + +SOURCE_ROOT_SUFFIXES = {".lua", ".luac"} + + +class SemanticEmitter: + def __init__(self, trap: TrapWriter, fixture_id: str, artifact_key: str): + self.trap = trap + self.fixture_id = fixture_id + self.artifact_key = artifact_key + self.normalizer = InstructionSemanticNormalizer() + self.prototype_labels: dict[str, TrapLabel] = {} + self.instruction_labels: dict[tuple[str, int], TrapLabel] = {} + self.closure_targets_by_ref: dict[str, str] = {} + + def pcslot(self, prototype_id: str, pc: int, slot: int) -> str: + return f"{prototype_id}@pc{pc}:r{slot}" + + def constant_text(self, chunk: Chunk, index: int) -> str | None: + if 0 <= index < len(chunk.constants): + return chunk.constants[index].text() + return None + + def emit_upvalues(self, proto: TrapLabel, chunk: Chunk, prototype_id: str) -> None: + for idx in range(chunk.num_upvalues): + name = chunk.upvalues[idx] if idx < len(chunk.upvalues) and chunk.upvalues[idx] else "unavailable" + mapping_state = "debug-derived-candidate" if name != "unavailable" else "stripped/unavailable" + provenance = ( + "bytecode-only,debug-info-derived" if name != "unavailable" else "bytecode-only" + ) + row = self.trap.label(f"upvalue;{self.fixture_id};{self.artifact_key};{prototype_id};{idx}") + self.trap.tuple( + "lua_upvalues", + row, + proto, + self.fixture_id, + f"{prototype_id}:u{idx}", + prototype_id, + idx, + name, + mapping_state, + provenance, + ) + + def emit_instruction(self, proto: TrapLabel, prototype_id: str, pc: int, instr: Instruction) -> TrapLabel: + row = self.trap.label(f"instruction;{self.fixture_id};{self.artifact_key};{prototype_id};{pc}") + self.instruction_labels[(prototype_id, pc)] = row + self.trap.tuple( + "lua_instructions", + row, + proto, + self.fixture_id, + prototype_id, + pc, + instr.opcode.name, + instr.a, + instr.b, + instr.c, + ) + return row + + def emit_register_event( + self, instr_label: TrapLabel, prototype_id: str, pc: int, kind: str, slot: int + ) -> None: + row = self.trap.label( + f"reg;{self.fixture_id};{self.artifact_key};{prototype_id};{pc};{kind};{slot}" + ) + self.trap.tuple( + "lua_register_events", + row, + instr_label, + self.fixture_id, + prototype_id, + pc, + kind, + slot, + self.pcslot(prototype_id, pc, slot), + ) + + def emit_semantic_step(self, instr_label: TrapLabel, source_ref: str, dest_ref: str, kind: str) -> None: + row = self.trap.label(f"step;{self.fixture_id};{self.artifact_key};{source_ref};{dest_ref};{kind}") + self.trap.tuple("lua_semantic_steps", row, instr_label, self.fixture_id, source_ref, dest_ref, kind) + + def emit_closure_value( + self, instr_label: TrapLabel, value_ref: str, target_prototype_id: str, provenance: str + ) -> None: + row = self.trap.label(f"closure;{self.fixture_id};{self.artifact_key};{value_ref}") + self.trap.tuple( + "lua_closure_values", + row, + instr_label, + self.fixture_id, + value_ref, + target_prototype_id, + provenance, + ) + self.closure_targets_by_ref[value_ref] = target_prototype_id + + def emit_call_site(self, instr_label: TrapLabel, prototype_id: str, item: CallSite) -> None: + row = self.trap.label(f"call;{self.fixture_id};{self.artifact_key};{item.callsite_id}") + pc = int(item.callsite_id.rsplit("@pc", 1)[1]) + self.trap.tuple( + "lua_call_sites", + row, + instr_label, + self.fixture_id, + item.callsite_id, + prototype_id, + pc, + item.opcode, + item.target_value_ref, + item.first_arg_slot, + item.arg_count, + item.first_return_slot, + item.return_count, + ) + + def emit_local_flow( + self, + prototype_id: str, + source_ref: str, + sink_ref: str, + edge_kind: str, + provenance: str = "bytecode-only", + ) -> None: + if source_ref == sink_ref: + return + row = self.trap.label( + f"local-flow;{self.fixture_id};{prototype_id};{source_ref};{sink_ref};{edge_kind}" + ) + self.trap.tuple( + "lua_local_flows", + row, + self.fixture_id, + prototype_id, + source_ref, + sink_ref, + edge_kind, + provenance, + ) + + def emit_analyzed_local_flow(self, flow: ValueFlowRelation) -> None: + if flow.module_path != self.fixture_id: + raise ValueError( + f"analysis flow module {flow.module_path!r} does not match {self.fixture_id!r}" + ) + self.emit_local_flow( + flow.prototype_id, + flow.source_ref, + flow.sink_ref, + flow.kind, + flow.provenance, + ) + + def emit_analyzed_control_flow_edge( + self, edge: ControlFlowEdgeRelation + ) -> None: + if edge.module_path != self.fixture_id: + raise ValueError( + f"control-flow edge module {edge.module_path!r} does not match " + f"{self.fixture_id!r}" + ) + source = self.instruction_labels[(edge.prototype_id, edge.source_pc)] + target = self.instruction_labels[(edge.prototype_id, edge.target_pc)] + row = self.trap.label( + f"control-flow-edge;{edge.module_path};{edge.prototype_id};" + f"{edge.source_pc};{edge.target_pc}" + ) + self.trap.tuple( + "lua_control_flow_edges", + row, + source, + target, + edge.module_path, + edge.prototype_id, + edge.source_pc, + edge.target_pc, + edge.provenance, + ) + + def emit_analyzed_dominator_tree_interval( + self, interval: DominatorTreeIntervalRelation + ) -> None: + if interval.module_path != self.fixture_id: + raise ValueError( + f"dominator interval module {interval.module_path!r} does not match " + f"{self.fixture_id!r}" + ) + instruction = self.instruction_labels[(interval.prototype_id, interval.pc)] + row = self.trap.label( + f"dominator-tree-interval;{interval.module_path};{interval.prototype_id};" + f"{interval.pc}" + ) + self.trap.tuple( + "lua_dominator_tree_intervals", + row, + instruction, + interval.module_path, + interval.prototype_id, + interval.pc, + interval.start, + interval.end, + interval.provenance, + ) + + def emit_analysis_boundary(self, boundary: AnalysisBoundary) -> None: + if boundary.module_path != self.fixture_id: + raise ValueError( + f"analysis boundary module {boundary.module_path!r} does not match " + f"{self.fixture_id!r}" + ) + row = self.trap.label( + f"analysis-boundary;{boundary.module_path};{boundary.prototype_id};" + f"{boundary.site_id};{boundary.boundary_kind}" + ) + self.trap.tuple( + "lua_analysis_boundaries", + row, + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + + def emit_analyzed_call_resolution(self, resolution: CallResolutionRelation) -> None: + if resolution.caller_module_path != self.fixture_id: + raise ValueError( + f"call resolution module {resolution.caller_module_path!r} does not match " + f"{self.fixture_id!r}" + ) + row = self.trap.label( + f"call-resolution;{resolution.caller_module_path};" + f"{resolution.caller_prototype_id};{resolution.callsite_id};" + f"{resolution.target_value_ref};{resolution.resolution_kind};" + f"{resolution.target_module_path};{resolution.target_prototype_id};" + f"{resolution.resolved_name}" + ) + self.trap.tuple( + "lua_call_resolutions", + row, + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + + def emit_analyzed_literal_require(self, require: LiteralRequireRelation) -> None: + if require.caller_module_path != self.fixture_id: + raise ValueError( + f"literal require module {require.caller_module_path!r} does not match " + f"{self.fixture_id!r}" + ) + row = self.trap.label( + f"literal-require;{require.caller_module_path};" + f"{require.caller_prototype_id};{require.callsite_id};" + f"{require.require_string};{require.argument_ref}" + ) + self.trap.tuple( + "lua_literal_requires", + row, + require.caller_module_path, + require.caller_prototype_id, + require.callsite_id, + require.require_string, + require.argument_ref, + require.provenance, + ) + + def emit_analyzed_module_resolution( + self, resolution: ModuleResolutionRelation + ) -> None: + if resolution.caller_module_path != self.fixture_id: + raise ValueError( + f"module resolution caller {resolution.caller_module_path!r} does not match " + f"{self.fixture_id!r}" + ) + row = self.trap.label( + f"module-resolution;{resolution.caller_module_path};" + f"{resolution.callsite_id};{resolution.require_string};{resolution.status};" + f"{resolution.target_module_path};{resolution.reason}" + ) + self.trap.tuple( + "lua_module_resolutions", + row, + resolution.caller_module_path, + resolution.callsite_id, + resolution.require_string, + resolution.status, + resolution.target_module_path, + resolution.reason, + resolution.provenance, + ) + + def emit_analyzed_module_export(self, export: ModuleExportRelation) -> None: + if export.module_path != self.fixture_id: + raise ValueError( + f"module export path {export.module_path!r} does not match " + f"{self.fixture_id!r}" + ) + row = self.trap.label( + f"module-export;{export.module_path};{export.export_kind};" + f"{export.field_name};{export.value_ref};{export.target_prototype_id};" + f"{export.provenance}" + ) + self.trap.tuple( + "lua_module_exports", + row, + export.module_path, + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + + def emit_analyzed_interprocedural_flow( + self, flow: InterproceduralFlowRelation + ) -> None: + if flow.caller_module_path != self.fixture_id: + raise ValueError( + f"interprocedural flow caller {flow.caller_module_path!r} does not match " + f"{self.fixture_id!r}" + ) + row = self.trap.label( + f"interprocedural-flow;{flow.caller_module_path};" + f"{flow.caller_prototype_id};{flow.callsite_id};" + f"{flow.callee_module_path};{flow.callee_prototype_id};" + f"{flow.source_ref};{flow.sink_ref};{flow.flow_kind};" + f"{flow.position};{flow.provenance}" + ) + self.trap.tuple( + "lua_interprocedural_flows", + row, + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + + def emit_analyzed_table_field_flow(self, flow: TableFieldFlowRelation) -> None: + if flow.module_path != self.fixture_id: + raise ValueError( + f"table flow module {flow.module_path!r} does not match {self.fixture_id!r}" + ) + row = self.trap.label( + f"table-flow;{flow.module_path};{flow.prototype_id};{flow.table_ref};" + f"{flow.field_name};{flow.write_ref};{flow.read_ref}" + ) + self.trap.tuple( + "lua_table_field_flows", + row, + flow.module_path, + flow.prototype_id, + flow.table_ref, + flow.field_name, + flow.write_ref, + flow.read_ref, + flow.provenance, + ) + self.emit_local_flow( + flow.prototype_id, + flow.write_ref, + flow.read_ref, + "table-write-to-read", + flow.provenance, + ) + + def emit_analyzed_global_flow(self, flow: GlobalFlowRelation) -> None: + if flow.module_path != self.fixture_id: + raise ValueError( + f"global flow module {flow.module_path!r} does not match {self.fixture_id!r}" + ) + row = self.trap.label( + f"global-flow;{flow.module_path};{flow.global_name};{flow.write_ref};" + f"{flow.read_ref};{flow.value_ref}" + ) + self.trap.tuple( + "lua_global_flows", + row, + flow.module_path, + flow.global_name, + flow.write_ref, + flow.read_ref, + flow.value_ref, + flow.provenance, + ) + self.emit_local_flow( + flow.prototype_id, + flow.value_ref, + flow.read_ref, + "global-write-to-read", + flow.provenance, + ) + + def emit_analyzed_upvalue_flow(self, flow: UpvalueFlowRelation) -> None: + if flow.module_path != self.fixture_id: + raise ValueError( + f"upvalue flow module {flow.module_path!r} does not match {self.fixture_id!r}" + ) + row = self.trap.label( + f"upvalue-flow;{flow.module_path};{flow.upvalue_id};{flow.capture_ref};" + f"{flow.read_ref};{flow.write_ref}" + ) + self.trap.tuple( + "lua_upvalue_flows", + row, + flow.module_path, + flow.upvalue_id, + flow.capture_ref, + flow.read_ref, + flow.write_ref, + flow.provenance, + ) + + def emit_instruction_semantics(self, prototype_id: str, pc: int, instr: Instruction, chunk: Chunk) -> None: + instr_label = self.instruction_labels[(prototype_id, pc)] + semantics = self.normalizer.normalize(prototype_id, pc, instr, chunk) + for item in semantics.effects: + if isinstance(item, RegisterEvent): + self.emit_register_event(instr_label, prototype_id, pc, item.kind, item.slot) + elif isinstance(item, SemanticStep): + self.emit_semantic_step(instr_label, item.source_ref, item.dest_ref, item.kind) + elif isinstance(item, ClosureValue): + self.emit_closure_value( + instr_label, item.value_ref, item.target_prototype_id, item.provenance + ) + elif isinstance(item, CallSite): + self.emit_call_site(instr_label, prototype_id, item) + else: + raise TypeError(f"unsupported instruction semantic effect: {type(item).__name__}") + + def emit_prototype_semantics(self, prototype_id: str, chunk: Chunk) -> None: + for pc, instr in enumerate(chunk.instructions): + self.emit_instruction_semantics(prototype_id, pc, instr, chunk) + for idx, child in enumerate(chunk.protos): + child_id = f"{prototype_id}.{idx}" if prototype_id else str(idx) + self.emit_prototype_semantics(child_id, child) + + +def display_path(path: Path) -> str: + try: + return str(path.resolve().relative_to(Path.cwd().resolve())) + except ValueError: + return str(path) + + +def copy_source(path: Path, source_archive_dir: Path) -> None: + archive_path = str(path.resolve()).replace(":", "_").lstrip("/\\") + target = source_archive_dir / archive_path + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, target) + + +def file_fact_labels( + trap: TrapWriter, path: Path +) -> tuple[TrapLabel, TrapLabel, TrapLabel, str]: + rel = display_path(path) + file_label = trap.label(f"{rel};sourcefile") + folder_label = trap.label(str(path.parent)) + loc_label = trap.label(f"loc,{rel},1,1,1,1") + return file_label, folder_label, loc_label, rel + + +def emit_file_fact_tuples( + trap: TrapWriter, + file_label: TrapLabel, + folder_label: TrapLabel, + loc_label: TrapLabel, + path: Path, + rel: str, +) -> None: + trap.tuple("files", file_label, rel) + trap.tuple("folders", folder_label, str(path.parent)) + trap.tuple("containerparent", folder_label, file_label) + trap.tuple("locations_default", loc_label, file_label, 1, 1, 1, 1) + + +def emit_lua_source_file(trap: TrapWriter, path: Path, source_archive_dir: Path) -> None: + file_label, folder_label, loc_label, rel = file_fact_labels(trap, path) + content = path.read_bytes() + text = content.decode("utf-8") + line_count = len(text.splitlines()) + byte_count = len(content) + sha256 = hashlib.sha256(content).hexdigest() + source_label = trap.label(f"lua-source-file;{rel}") + emit_file_fact_tuples(trap, file_label, folder_label, loc_label, path, rel) + trap.tuple("lua_source_files", source_label, file_label, rel, line_count, byte_count, sha256) + copy_source(path, source_archive_dir) + + +def debug_metadata_state(chunk: Chunk) -> tuple[str, str]: + if chunk.name: + return "debug-derived-candidate", "bytecode-only,debug-info-derived" + return "stripped/unavailable", "bytecode-only" + + +def emit_profile(trap: TrapWriter, artifact: TrapLabel, loaded: LoadedArtifact) -> None: + profile = loaded.profile + trap.tuple( + "lua_profiles", + artifact, + f"0x{profile['version']:02x}", + profile["format"], + profile["little_endian"], + profile["int_size"], + profile["size_t_size"], + profile["instruction_size"], + profile["lua_number_size"], + profile["integral_flag"], + ) + + +def emit_chunk( + trap: TrapWriter, + artifact: TrapLabel, + fixture_id: str, + artifact_key: str, + chunk: Chunk, + prototype_id: str, + parent_id: str, + ordinal_index: int, + semantic_emitter: SemanticEmitter | None = None, +) -> None: + proto = trap.label(f"prototype;{fixture_id};{artifact_key};{prototype_id}") + if semantic_emitter is not None: + semantic_emitter.prototype_labels[prototype_id] = proto + debug_name = chunk.name or "unavailable" + mapping_state, provenance = debug_metadata_state(chunk) + trap.tuple( + "lua_prototypes", + proto, + artifact, + fixture_id, + prototype_id, + parent_id, + ordinal_index, + chunk.num_params, + 1 if chunk.is_vararg else 0, + chunk.max_stack, + chunk.num_upvalues, + debug_name, + mapping_state, + provenance, + ) + + for idx, const in enumerate(chunk.constants): + const_label = trap.label(f"constant;{fixture_id};{artifact_key};{prototype_id};{idx}") + trap.tuple( + "lua_constants", + const_label, + proto, + fixture_id, + f"{prototype_id}:k{idx}", + prototype_id, + idx, + const.type_name, + const.text(), + ) + + if semantic_emitter is not None: + semantic_emitter.emit_upvalues(proto, chunk, prototype_id) + for pc, instr in enumerate(chunk.instructions): + semantic_emitter.emit_instruction(proto, prototype_id, pc, instr) + + for idx, child in enumerate(chunk.protos): + child_id = f"{prototype_id}.{idx}" if prototype_id else str(idx) + emit_chunk( + trap, + artifact, + fixture_id, + artifact_key, + child, + child_id, + prototype_id, + idx, + semantic_emitter, + ) + + +def emit_loaded_bytecode( + trap: TrapWriter, + path: Path, + source_archive_dir: Path, + loaded: LoadedArtifact, + identity: ArtifactIdentityRelation, + value_flows: tuple[ValueFlowRelation, ...], + control_flow_edges: tuple[ControlFlowEdgeRelation, ...], + dominator_tree_intervals: tuple[DominatorTreeIntervalRelation, ...], + boundaries: tuple[AnalysisBoundary, ...], + call_resolutions: tuple[CallResolutionRelation, ...], + literal_requires: tuple[LiteralRequireRelation, ...], + module_resolutions: tuple[ModuleResolutionRelation, ...], + module_exports: tuple[ModuleExportRelation, ...], + interprocedural_flows: tuple[InterproceduralFlowRelation, ...], + table_field_flows: tuple[TableFieldFlowRelation, ...], + global_flows: tuple[GlobalFlowRelation, ...], + upvalue_flows: tuple[UpvalueFlowRelation, ...], +) -> None: + file_label, folder_label, loc_label, _ = file_fact_labels(trap, path) + rel = identity.module_path + emit_file_fact_tuples(trap, file_label, folder_label, loc_label, path, rel) + copy_source(path, source_archive_dir) + fixture_id = rel + artifact = trap.label(f"artifact;{fixture_id};{rel}") + trap.tuple( + "lua_artifacts", + artifact, + fixture_id, + rel, + "bytecode-only", + loaded.profile_id, + 1, + "bytecode-only", + ) + emit_profile(trap, artifact, loaded) + if loaded.chunk is None: + raise RuntimeError(f"{rel}: accepted bytecode has no loaded chunk") + semantic_emitter = SemanticEmitter(trap, fixture_id, rel) + emit_chunk(trap, artifact, fixture_id, rel, loaded.chunk, "root", "", -1, semantic_emitter) + semantic_emitter.emit_prototype_semantics("root", loaded.chunk) + for flow in value_flows: + semantic_emitter.emit_analyzed_local_flow(flow) + for edge in control_flow_edges: + semantic_emitter.emit_analyzed_control_flow_edge(edge) + for interval in dominator_tree_intervals: + semantic_emitter.emit_analyzed_dominator_tree_interval(interval) + for boundary in boundaries: + semantic_emitter.emit_analysis_boundary(boundary) + for resolution in call_resolutions: + semantic_emitter.emit_analyzed_call_resolution(resolution) + for require in literal_requires: + semantic_emitter.emit_analyzed_literal_require(require) + for resolution in module_resolutions: + semantic_emitter.emit_analyzed_module_resolution(resolution) + for export in module_exports: + semantic_emitter.emit_analyzed_module_export(export) + for flow in interprocedural_flows: + semantic_emitter.emit_analyzed_interprocedural_flow(flow) + for flow in table_field_flows: + semantic_emitter.emit_analyzed_table_field_flow(flow) + for flow in global_flows: + semantic_emitter.emit_analyzed_global_flow(flow) + for flow in upvalue_flows: + semantic_emitter.emit_analyzed_upvalue_flow(flow) + + +def emit_bytecode_diagnostic(trap: TrapWriter, path: Path, source_archive_dir: Path, exc: BytecodeError) -> None: + file_label, folder_label, loc_label, rel = file_fact_labels(trap, path) + emit_file_fact_tuples(trap, file_label, folder_label, loc_label, path, rel) + copy_source(path, source_archive_dir) + fixture_id = rel + diagnostic_kind = exc.diagnostic_kind + message_category = exc.message_category + artifact = trap.label(f"artifact;{fixture_id};{rel}") + trap.tuple( + "lua_artifacts", + artifact, + fixture_id, + rel, + "malformed-bytecode", + "unavailable", + 0, + "malformed/unsupported", + ) + diag = trap.label(f"diagnostic;{fixture_id};{diagnostic_kind};{rel}") + trap.tuple( + "lua_diagnostics", + diag, + artifact, + fixture_id, + f"{fixture_id}:{diagnostic_kind}", + diagnostic_kind, + rel, + "error", + message_category, + 0, + "malformed/unsupported", + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--file-list") + parser.add_argument("--source-root") + parser.add_argument("--source-archive-dir", required=True) + parser.add_argument("--output-dir", required=True) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + trap = TrapWriter() + source_archive_dir = Path(args.source_archive_dir) + if args.file_list: + paths = [ + Path(line) + for line in Path(args.file_list).read_text(encoding="utf-8").splitlines() + if line + ] + elif args.source_root: + paths = sorted( + path + for path in Path(args.source_root).rglob("*") + if path.is_file() and path.suffix in SOURCE_ROOT_SUFFIXES + ) + else: + raise SystemExit("Either --file-list or --source-root is required") + + accepted_bytecode: list[tuple[Path, LoadedArtifact]] = [] + for path in paths: + if path.suffix == ".lua": + emit_lua_source_file(trap, path, source_archive_dir) + elif path.suffix == ".luac": + try: + loaded = Lua51Loader(path.read_bytes()).load() + except BytecodeError as exc: + emit_bytecode_diagnostic(trap, path, source_archive_dir, exc) + continue + accepted_bytecode.append((path, loaded)) + + analysis = analyze_corpus( + AcceptedCorpus( + artifacts=tuple( + CorpusArtifact(module_path=display_path(path), loaded_artifact=loaded) + for path, loaded in accepted_bytecode + ) + ) + ) + identity_by_module = { + identity.module_path: identity for identity in analysis.artifact_identities + } + value_flows_by_module: dict[str, list[ValueFlowRelation]] = {} + for flow in analysis.value_flows: + value_flows_by_module.setdefault(flow.module_path, []).append(flow) + control_flow_edges_by_module: dict[str, list[ControlFlowEdgeRelation]] = {} + for edge in analysis.control_flow_edges: + control_flow_edges_by_module.setdefault(edge.module_path, []).append(edge) + dominator_tree_intervals_by_module: dict[ + str, list[DominatorTreeIntervalRelation] + ] = {} + for interval in analysis.dominator_tree_intervals: + dominator_tree_intervals_by_module.setdefault( + interval.module_path, [] + ).append(interval) + boundaries_by_module: dict[str, list[AnalysisBoundary]] = {} + for boundary in analysis.boundaries: + boundaries_by_module.setdefault(boundary.module_path, []).append(boundary) + call_resolutions_by_module: dict[str, list[CallResolutionRelation]] = {} + for resolution in analysis.call_resolutions: + call_resolutions_by_module.setdefault( + resolution.caller_module_path, [] + ).append(resolution) + literal_requires_by_module: dict[str, list[LiteralRequireRelation]] = {} + for require in analysis.literal_requires: + literal_requires_by_module.setdefault( + require.caller_module_path, [] + ).append(require) + module_resolutions_by_module: dict[str, list[ModuleResolutionRelation]] = {} + for resolution in analysis.module_resolutions: + module_resolutions_by_module.setdefault( + resolution.caller_module_path, [] + ).append(resolution) + module_exports_by_module: dict[str, list[ModuleExportRelation]] = {} + for export in analysis.module_exports: + module_exports_by_module.setdefault(export.module_path, []).append(export) + interprocedural_flows_by_module: dict[str, list[InterproceduralFlowRelation]] = {} + for flow in analysis.interprocedural_flows: + interprocedural_flows_by_module.setdefault( + flow.caller_module_path, [] + ).append(flow) + table_field_flows_by_module: dict[str, list[TableFieldFlowRelation]] = {} + for flow in analysis.table_field_flows: + table_field_flows_by_module.setdefault(flow.module_path, []).append(flow) + global_flows_by_module: dict[str, list[GlobalFlowRelation]] = {} + for flow in analysis.global_flows: + global_flows_by_module.setdefault(flow.module_path, []).append(flow) + upvalue_flows_by_module: dict[str, list[UpvalueFlowRelation]] = {} + for flow in analysis.upvalue_flows: + upvalue_flows_by_module.setdefault(flow.module_path, []).append(flow) + for path, loaded in accepted_bytecode: + module_path = display_path(path) + emit_loaded_bytecode( + trap, + path, + source_archive_dir, + loaded, + identity_by_module[module_path], + tuple(value_flows_by_module.get(module_path, ())), + tuple(control_flow_edges_by_module.get(module_path, ())), + tuple(dominator_tree_intervals_by_module.get(module_path, ())), + tuple(boundaries_by_module.get(module_path, ())), + tuple(call_resolutions_by_module.get(module_path, ())), + tuple(literal_requires_by_module.get(module_path, ())), + tuple(module_resolutions_by_module.get(module_path, ())), + tuple(module_exports_by_module.get(module_path, ())), + tuple(interprocedural_flows_by_module.get(module_path, ())), + tuple(table_field_flows_by_module.get(module_path, ())), + tuple(global_flows_by_module.get(module_path, ())), + tuple(upvalue_flows_by_module.get(module_path, ())), + ) + + trap.write(Path(args.output_dir) / "lua-source-files.trap") + + +if __name__ == "__main__": + main() diff --git a/lua/tools/lua_bytecode.py b/lua/tools/lua_bytecode.py new file mode 100644 index 000000000000..3bc54169ae5b --- /dev/null +++ b/lua/tools/lua_bytecode.py @@ -0,0 +1,391 @@ +"""Lua 5.1 bytecode loading and header validation for CodeQL facts.""" + +from __future__ import annotations + +import struct +from dataclasses import dataclass, field +from enum import IntEnum +from typing import Any + + +LUA_MAGIC = b"\x1bLua" +PROFILE_FIELDS = ( + "version", + "format", + "little_endian", + "int_size", + "size_t_size", + "instruction_size", + "lua_number_size", + "integral_flag", +) +LUA51_VERSION = 0x51 + + +class BytecodeError(Exception): + diagnostic_kind = "truncated-bytecode" + message_category = "bytecode-error" + + +class NotLuaBytecode(BytecodeError): + diagnostic_kind = "not-lua-bytecode" + message_category = "bad-magic" + + +class TruncatedBytecode(BytecodeError): + diagnostic_kind = "truncated-bytecode" + message_category = "unexpected-eof" + + +class UnsupportedVersion(BytecodeError): + diagnostic_kind = "unsupported-bytecode-version" + message_category = "unsupported-version" + + +class UnsupportedProfile(BytecodeError): + diagnostic_kind = "unsupported-bytecode-profile" + message_category = "unsupported-profile" + + def __init__(self, message: str, message_category: str = "unsupported-profile"): + super().__init__(message) + self.message_category = message_category + + +class MalformedConstant(BytecodeError): + diagnostic_kind = "malformed-constant" + message_category = "bad-constant" + + +class MalformedOpcode(BytecodeError): + diagnostic_kind = "malformed-opcode" + message_category = "invalid-opcode" + + +class InstructionType(IntEnum): + ABC = 0 + ABx = 1 + AsBx = 2 + + +class Opcode(IntEnum): + MOVE = 0 + LOADK = 1 + LOADBOOL = 2 + LOADNIL = 3 + GETUPVAL = 4 + GETGLOBAL = 5 + GETTABLE = 6 + SETGLOBAL = 7 + SETUPVAL = 8 + SETTABLE = 9 + NEWTABLE = 10 + SELF = 11 + ADD = 12 + SUB = 13 + MUL = 14 + DIV = 15 + MOD = 16 + POW = 17 + UNM = 18 + NOT = 19 + LEN = 20 + CONCAT = 21 + JMP = 22 + EQ = 23 + LT = 24 + LE = 25 + TEST = 26 + TESTSET = 27 + CALL = 28 + TAILCALL = 29 + RETURN = 30 + FORLOOP = 31 + FORPREP = 32 + TFORLOOP = 33 + SETLIST = 34 + CLOSE = 35 + CLOSURE = 36 + VARARG = 37 + + +INSTRUCTION_TYPES = { + Opcode.MOVE: InstructionType.ABC, + Opcode.LOADK: InstructionType.ABx, + Opcode.LOADBOOL: InstructionType.ABC, + Opcode.LOADNIL: InstructionType.ABC, + Opcode.GETUPVAL: InstructionType.ABC, + Opcode.GETGLOBAL: InstructionType.ABx, + Opcode.GETTABLE: InstructionType.ABC, + Opcode.SETGLOBAL: InstructionType.ABx, + Opcode.SETUPVAL: InstructionType.ABC, + Opcode.SETTABLE: InstructionType.ABC, + Opcode.NEWTABLE: InstructionType.ABC, + Opcode.SELF: InstructionType.ABC, + Opcode.ADD: InstructionType.ABC, + Opcode.SUB: InstructionType.ABC, + Opcode.MUL: InstructionType.ABC, + Opcode.DIV: InstructionType.ABC, + Opcode.MOD: InstructionType.ABC, + Opcode.POW: InstructionType.ABC, + Opcode.UNM: InstructionType.ABC, + Opcode.NOT: InstructionType.ABC, + Opcode.LEN: InstructionType.ABC, + Opcode.CONCAT: InstructionType.ABC, + Opcode.JMP: InstructionType.AsBx, + Opcode.EQ: InstructionType.ABC, + Opcode.LT: InstructionType.ABC, + Opcode.LE: InstructionType.ABC, + Opcode.TEST: InstructionType.ABC, + Opcode.TESTSET: InstructionType.ABC, + Opcode.CALL: InstructionType.ABC, + Opcode.TAILCALL: InstructionType.ABC, + Opcode.RETURN: InstructionType.ABC, + Opcode.FORLOOP: InstructionType.AsBx, + Opcode.FORPREP: InstructionType.AsBx, + Opcode.TFORLOOP: InstructionType.ABC, + Opcode.SETLIST: InstructionType.ABC, + Opcode.CLOSE: InstructionType.ABC, + Opcode.CLOSURE: InstructionType.ABx, + Opcode.VARARG: InstructionType.ABC, +} + + +class ConstType(IntEnum): + NIL = 0 + BOOL = 1 + NUMBER = 3 + STRING = 4 + + +@dataclass +class Instruction: + opcode: Opcode + op_type: InstructionType + a: int + b: int + c: int = -1 + + +@dataclass +class Constant: + type_name: str + value: Any + + def text(self) -> str: + if self.value is None: + return "nil" + if self.type_name == "bool": + return "true" if self.value else "false" + return str(self.value) + + +@dataclass +class Local: + name: str + start: int + end: int + + +@dataclass +class Chunk: + name: str = "" + first_line: int = 0 + last_line: int = 0 + num_upvalues: int = 0 + num_params: int = 0 + is_vararg: bool = False + max_stack: int = 0 + instructions: list[Instruction] = field(default_factory=list) + constants: list[Constant] = field(default_factory=list) + protos: list["Chunk"] = field(default_factory=list) + locals: list[Local] = field(default_factory=list) + upvalues: list[str] = field(default_factory=list) + + +@dataclass +class LoadedArtifact: + chunk: Chunk | None + profile: dict[str, int] + profile_id: str = "unavailable" + diagnostic_kind: str | None = None + message_category: str | None = None + accepted: bool = False + + +def bits(num: int, pos: int, size: int) -> int: + return (num >> pos) & (~((~0) << size)) + + +def is_k(rk: int) -> bool: + return (rk & (1 << 8)) > 0 + + +def rk_k_index(rk: int) -> int: + return rk & ~(1 << 8) + + +def profile_id_from_header(profile: dict[str, int]) -> str: + endian = "little" if profile["little_endian"] else "big" + number_mode = "integral" if profile["integral_flag"] else "float" + return ( + f"lua51-{endian}-int{profile['int_size']}-size_t{profile['size_t_size']}" + f"-instruction{profile['instruction_size']}-number{profile['lua_number_size']}-{number_mode}" + ) + + +def validate_implemented_header(profile: dict[str, int], *, endian_flag: int) -> None: + if profile["format"] != 0: + raise UnsupportedProfile( + f"unsupported Lua 5.1 bytecode format {profile['format']}", + "unsupported-format", + ) + if endian_flag not in {0, 1}: + raise UnsupportedProfile( + f"unsupported Lua 5.1 endianness flag {endian_flag}", + "unsupported-endianness", + ) + if profile["int_size"] <= 0: + raise UnsupportedProfile( + f"unsupported Lua 5.1 int size {profile['int_size']}", + "unsupported-int-size", + ) + if profile["size_t_size"] <= 0: + raise UnsupportedProfile( + f"unsupported Lua 5.1 size_t size {profile['size_t_size']}", + "unsupported-size_t-size", + ) + if profile["instruction_size"] != 4: + raise UnsupportedProfile( + f"unsupported Lua 5.1 instruction size {profile['instruction_size']}", + "unsupported-instruction-size", + ) + if profile["lua_number_size"] != 8: + raise UnsupportedProfile( + f"unsupported Lua 5.1 lua_Number size {profile['lua_number_size']}", + "unsupported-number-size", + ) + if profile["integral_flag"] != 0: + raise UnsupportedProfile( + f"unsupported Lua 5.1 number mode integral_flag={profile['integral_flag']}", + "unsupported-number-mode", + ) + + +class Lua51Loader: + """Load one Lua 5.1 bytecode artifact using header-driven field sizes.""" + + def __init__(self, data: bytes): + self.data = data + self.index = 0 + self.profile: dict[str, int] = {} + self.endian_flag = 1 + + def read(self, size: int) -> bytes: + if self.index + size > len(self.data): + raise TruncatedBytecode("unexpected end of Lua bytecode") + out = self.data[self.index : self.index + size] + self.index += size + return out + + def byte(self) -> int: + return self.read(1)[0] + + def uint(self) -> int: + return int.from_bytes(self.read(self.profile["int_size"]), self.endian(), signed=False) + + def uint32(self) -> int: + return int.from_bytes(self.read(4), self.endian(), signed=False) + + def size_t(self) -> int: + return int.from_bytes(self.read(self.profile["size_t_size"]), self.endian(), signed=False) + + def number(self) -> float: + if self.profile["lua_number_size"] != 8: + raise UnsupportedProfile( + "unsupported Lua 5.1 lua_Number size", + "unsupported-number-size", + ) + return struct.unpack("d", self.read(8))[0] + + def endian(self) -> str: + return "little" if self.profile["little_endian"] else "big" + + def string(self) -> str: + size = self.size_t() + if size == 0: + return "" + raw = self.read(size) + return raw[:-1].decode("latin-1") + + def load(self) -> LoadedArtifact: + if len(self.data) < 4 or self.data[:4] != LUA_MAGIC: + raise NotLuaBytecode("Lua bytecode magic expected") + self.index = 4 + version = self.byte() + format_byte = self.byte() + self.endian_flag = self.byte() + self.profile = { + "version": version, + "format": format_byte, + "little_endian": 1 if self.endian_flag == 1 else 0, + "int_size": self.byte(), + "size_t_size": self.byte(), + "instruction_size": self.byte(), + "lua_number_size": self.byte(), + "integral_flag": self.byte(), + } + if self.profile["version"] != LUA51_VERSION: + raise UnsupportedVersion("unsupported Lua bytecode version") + validate_implemented_header(self.profile, endian_flag=self.endian_flag) + profile_id = profile_id_from_header(self.profile) + return LoadedArtifact(self.chunk(), self.profile, profile_id=profile_id, accepted=True) + + def chunk(self) -> Chunk: + chunk = Chunk() + chunk.name = self.string() + chunk.first_line = self.uint() + chunk.last_line = self.uint() + chunk.num_upvalues = self.byte() + chunk.num_params = self.byte() + chunk.is_vararg = self.byte() != 0 + chunk.max_stack = self.byte() + for _ in range(self.uint()): + chunk.instructions.append(self.instruction()) + for _ in range(self.uint()): + chunk.constants.append(self.constant()) + for _ in range(self.uint()): + chunk.protos.append(self.chunk()) + for _ in range(self.uint()): + self.uint() + for _ in range(self.uint()): + chunk.locals.append(Local(self.string(), self.uint(), self.uint())) + for _ in range(self.uint()): + chunk.upvalues.append(self.string()) + return chunk + + def instruction(self) -> Instruction: + raw = self.uint32() + opcode_value = bits(raw, 0, 6) + if opcode_value not in Opcode._value2member_map_: + raise MalformedOpcode(f"unknown Lua opcode {opcode_value}") + op = Opcode(opcode_value) + op_type = INSTRUCTION_TYPES[op] + a = bits(raw, 6, 8) + if op_type == InstructionType.ABC: + return Instruction(op, op_type, a, bits(raw, 23, 9), bits(raw, 14, 9)) + b = bits(raw, 14, 18) + if op_type == InstructionType.AsBx: + b -= 131071 + return Instruction(op, op_type, a, b) + + def constant(self) -> Constant: + tag = self.byte() + if tag == ConstType.NIL: + return Constant("nil", None) + if tag == ConstType.BOOL: + return Constant("bool", self.byte() != 0) + if tag == ConstType.NUMBER: + return Constant("number", self.number()) + if tag == ConstType.STRING: + return Constant("string", self.string()) + raise MalformedConstant(f"unknown Lua constant type {tag}") diff --git a/lua/tools/semantic_normalizer.py b/lua/tools/semantic_normalizer.py new file mode 100644 index 000000000000..c78f7ffc6810 --- /dev/null +++ b/lua/tools/semantic_normalizer.py @@ -0,0 +1,137 @@ +"""Normalize Lua bytecode instructions into extractor-owned semantic facts.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from lua_bytecode import Chunk, Instruction, Opcode, is_k + + +@dataclass(frozen=True) +class RegisterEvent: + kind: str + slot: int + + +@dataclass(frozen=True) +class SemanticStep: + source_ref: str + dest_ref: str + kind: str + + +@dataclass(frozen=True) +class ClosureValue: + slot: int + value_ref: str + target_prototype_id: str + provenance: str + + +@dataclass(frozen=True) +class CallSite: + callsite_id: str + opcode: str + target_value_ref: str + first_arg_slot: int + arg_count: int + first_return_slot: int + return_count: int + + +@dataclass(frozen=True) +class InstructionSemantics: + effects: list[RegisterEvent | SemanticStep | ClosureValue | CallSite] + + +class InstructionSemanticNormalizer: + """Produce bytecode-level normalized facts, not final flow/taint results.""" + + def pcslot(self, prototype_id: str, pc: int, slot: int) -> str: + return f"{prototype_id}@pc{pc}:r{slot}" + + def normalize(self, prototype_id: str, pc: int, instr: Instruction, chunk: Chunk) -> InstructionSemantics: + effects: list[RegisterEvent | SemanticStep | ClosureValue | CallSite] = [] + + def event(kind: str, slot: int) -> None: + effects.append(RegisterEvent(kind, slot)) + + def step(src_slot: int, dst_slot: int, kind: str) -> None: + effects.append( + SemanticStep( + self.pcslot(prototype_id, pc, src_slot), + self.pcslot(prototype_id, pc, dst_slot), + kind, + ) + ) + + op = instr.opcode + a, b, c = instr.a, instr.b, instr.c + if op == Opcode.MOVE: + event("read", b) + event("write", a) + step(b, a, "move") + elif op == Opcode.LOADK: + event("write", a) + elif op == Opcode.CLOSURE: + event("write", a) + effects.append(ClosureValue(a, self.pcslot(prototype_id, pc, a), f"{prototype_id}.{b}", "bytecode-only")) + elif op in (Opcode.CALL, Opcode.TAILCALL): + arg_count = b - 1 if b > 0 else max(chunk.max_stack - (a + 1), 0) + ret_count = c - 1 if c > 0 else -1 + event("read", a) + for slot in range(a + 1, a + 1 + max(arg_count, 0)): + event("read", slot) + if ret_count == -1: + event("write", a) + else: + for slot in range(a, a + ret_count): + event("write", slot) + effects.append( + CallSite( + f"{prototype_id}@pc{pc}", + op.name, + self.pcslot(prototype_id, pc, a), + a + 1, + arg_count, + a, + ret_count, + ) + ) + elif op == Opcode.CONCAT: + for slot in range(b, c + 1): + event("read", slot) + step(slot, a, "concat") + event("write", a) + elif op == Opcode.GETUPVAL: + event("write", a) + elif op == Opcode.GETGLOBAL: + event("write", a) + elif op == Opcode.SETGLOBAL: + event("read", a) + elif op == Opcode.GETTABLE: + event("read", b) + event("write", a) + elif op == Opcode.SELF: + event("read", b) + event("write", a) + event("write", a + 1) + elif op in (Opcode.SETTABLE,): + event("read", a) + if not is_k(b): + event("read", b) + if not is_k(c): + event("read", c) + elif op == Opcode.NEWTABLE: + event("write", a) + elif op == Opcode.RETURN: + count = b - 1 if b > 0 else max(chunk.max_stack - a, 0) + for slot in range(a, a + count): + event("read", slot) + elif op in (Opcode.EQ, Opcode.LT, Opcode.LE): + if not is_k(b): + event("read", b) + if not is_k(c): + event("read", c) + + return InstructionSemantics(effects) diff --git a/lua/tools/tests/fixtures/arithmetic_concat.lua.txt b/lua/tools/tests/fixtures/arithmetic_concat.lua.txt new file mode 100644 index 000000000000..b86dafa97354 --- /dev/null +++ b/lua/tools/tests/fixtures/arithmetic_concat.lua.txt @@ -0,0 +1,8 @@ +local function calculate(first, second, third) + local sum = first + second + local shifted = sum + 1 + first = first .. second .. third + return first, shifted +end + +return calculate diff --git a/lua/tools/tests/fixtures/arithmetic_concat.luac b/lua/tools/tests/fixtures/arithmetic_concat.luac new file mode 100644 index 000000000000..a66110403033 Binary files /dev/null and b/lua/tools/tests/fixtures/arithmetic_concat.luac differ diff --git a/lua/tools/tests/fixtures/branch_merge.lua.txt b/lua/tools/tests/fixtures/branch_merge.lua.txt new file mode 100644 index 000000000000..6ce17b49a5d7 --- /dev/null +++ b/lua/tools/tests/fixtures/branch_merge.lua.txt @@ -0,0 +1,9 @@ +local function choose(condition, original, replacement) + local value = original + if condition then + value = replacement + end + return value +end + +return choose diff --git a/lua/tools/tests/fixtures/branch_merge.luac b/lua/tools/tests/fixtures/branch_merge.luac new file mode 100644 index 000000000000..28c61aeae1a6 Binary files /dev/null and b/lua/tools/tests/fixtures/branch_merge.luac differ diff --git a/lua/tools/tests/fixtures/call_result_table.lua b/lua/tools/tests/fixtures/call_result_table.lua new file mode 100644 index 000000000000..9ff8278eef07 --- /dev/null +++ b/lua/tools/tests/fixtures/call_result_table.lua @@ -0,0 +1,11 @@ +local function make_table() + return {} +end + +local function read_after_write(value, key) + local state = make_table() + state[key] = value + return state.result +end + +return read_after_write diff --git a/lua/tools/tests/fixtures/call_result_table.luac b/lua/tools/tests/fixtures/call_result_table.luac new file mode 100644 index 000000000000..18ca35f7edca Binary files /dev/null and b/lua/tools/tests/fixtures/call_result_table.luac differ diff --git a/lua/tools/tests/fixtures/captured_local_table_field_closure.lua b/lua/tools/tests/fixtures/captured_local_table_field_closure.lua new file mode 100644 index 000000000000..082a7909535f --- /dev/null +++ b/lua/tools/tests/fixtures/captured_local_table_field_closure.lua @@ -0,0 +1,11 @@ +local function send(value) + return value +end + +local transport = { send = send } + +local function dispatch(value) + return transport.send(value) +end + +return dispatch diff --git a/lua/tools/tests/fixtures/captured_local_table_field_closure.luac b/lua/tools/tests/fixtures/captured_local_table_field_closure.luac new file mode 100644 index 000000000000..f4a703e0e8a8 Binary files /dev/null and b/lua/tools/tests/fixtures/captured_local_table_field_closure.luac differ diff --git a/lua/tools/tests/fixtures/captured_local_table_field_overwrite.lua b/lua/tools/tests/fixtures/captured_local_table_field_overwrite.lua new file mode 100644 index 000000000000..5fd68d82e88d --- /dev/null +++ b/lua/tools/tests/fixtures/captured_local_table_field_overwrite.lua @@ -0,0 +1,12 @@ +local function send(value) + return value +end + +local transport = { send = send } +transport.send = "disabled" + +local function dispatch(value) + return transport.send(value) +end + +return dispatch diff --git a/lua/tools/tests/fixtures/captured_local_table_field_overwrite.luac b/lua/tools/tests/fixtures/captured_local_table_field_overwrite.luac new file mode 100644 index 000000000000..6a1dedc44bca Binary files /dev/null and b/lua/tools/tests/fixtures/captured_local_table_field_overwrite.luac differ diff --git a/lua/tools/tests/fixtures/close_effect.lua.txt b/lua/tools/tests/fixtures/close_effect.lua.txt new file mode 100644 index 000000000000..62eeac5063ab --- /dev/null +++ b/lua/tools/tests/fixtures/close_effect.lua.txt @@ -0,0 +1,13 @@ +local function preserve(value) + local result = value + do + local captured = value + local function read() + return captured + end + result = read() + end + return result +end + +return preserve diff --git a/lua/tools/tests/fixtures/close_effect.luac b/lua/tools/tests/fixtures/close_effect.luac new file mode 100644 index 000000000000..59c6b64edd81 Binary files /dev/null and b/lua/tools/tests/fixtures/close_effect.luac differ diff --git a/lua/tools/tests/fixtures/conditional_effects.lua.txt b/lua/tools/tests/fixtures/conditional_effects.lua.txt new file mode 100644 index 000000000000..16f33de0b316 --- /dev/null +++ b/lua/tools/tests/fixtures/conditional_effects.lua.txt @@ -0,0 +1,9 @@ +local function conditional_effects(first, second, replacement) + local selected = first or replacement + local less = first < second + local equal = first == second + local less_or_equal = first <= second + return selected, less, equal, less_or_equal +end + +return conditional_effects diff --git a/lua/tools/tests/fixtures/conditional_effects.luac b/lua/tools/tests/fixtures/conditional_effects.luac new file mode 100644 index 000000000000..a52acb2d22f6 Binary files /dev/null and b/lua/tools/tests/fixtures/conditional_effects.luac differ diff --git a/lua/tools/tests/fixtures/conflicting_literal_require.lua b/lua/tools/tests/fixtures/conflicting_literal_require.lua new file mode 100644 index 000000000000..bdc0c7133eb6 --- /dev/null +++ b/lua/tools/tests/fixtures/conflicting_literal_require.lua @@ -0,0 +1,9 @@ +local module_name + +if condition then + module_name = "alpha.module" +else + module_name = "beta.module" +end + +return require(module_name) diff --git a/lua/tools/tests/fixtures/conflicting_literal_require.luac b/lua/tools/tests/fixtures/conflicting_literal_require.luac new file mode 100644 index 000000000000..8bb50e6c540a Binary files /dev/null and b/lua/tools/tests/fixtures/conflicting_literal_require.luac differ diff --git a/lua/tools/tests/fixtures/fixed_vararg.lua.txt b/lua/tools/tests/fixtures/fixed_vararg.lua.txt new file mode 100644 index 000000000000..9b572322381b --- /dev/null +++ b/lua/tools/tests/fixtures/fixed_vararg.lua.txt @@ -0,0 +1,6 @@ +local function collect(...) + local first, second, third = ... + return first, second, third +end + +return collect diff --git a/lua/tools/tests/fixtures/fixed_vararg.luac b/lua/tools/tests/fixtures/fixed_vararg.luac new file mode 100644 index 000000000000..9d76ff802741 Binary files /dev/null and b/lua/tools/tests/fixtures/fixed_vararg.luac differ diff --git a/lua/tools/tests/fixtures/generic_loop.lua.txt b/lua/tools/tests/fixtures/generic_loop.lua.txt new file mode 100644 index 000000000000..6f69a83fb17e --- /dev/null +++ b/lua/tools/tests/fixtures/generic_loop.lua.txt @@ -0,0 +1,9 @@ +local function last_value(iterator, state, control) + local result = control + for _, value in iterator, state, control do + result = value + end + return result +end + +return last_value diff --git a/lua/tools/tests/fixtures/generic_loop.luac b/lua/tools/tests/fixtures/generic_loop.luac new file mode 100644 index 000000000000..71bc5b9c3d92 Binary files /dev/null and b/lua/tools/tests/fixtures/generic_loop.luac differ diff --git a/lua/tools/tests/fixtures/mixed_fixed_open_call.lua b/lua/tools/tests/fixtures/mixed_fixed_open_call.lua new file mode 100644 index 000000000000..cbe5e1e7b317 --- /dev/null +++ b/lua/tools/tests/fixtures/mixed_fixed_open_call.lua @@ -0,0 +1,17 @@ +local function fixed(value) + return value +end + +local function open(value) + return value, value +end + +local function consume(...) + return ... +end + +local function run(first, second) + return consume(fixed(first), open(second)) +end + +return run diff --git a/lua/tools/tests/fixtures/mixed_fixed_open_call.luac b/lua/tools/tests/fixtures/mixed_fixed_open_call.luac new file mode 100644 index 000000000000..43233c9a2855 Binary files /dev/null and b/lua/tools/tests/fixtures/mixed_fixed_open_call.luac differ diff --git a/lua/tools/tests/fixtures/module_global_export.lua b/lua/tools/tests/fixtures/module_global_export.lua new file mode 100644 index 000000000000..276858fd01a3 --- /dev/null +++ b/lua/tools/tests/fixtures/module_global_export.lua @@ -0,0 +1,5 @@ +module("sample") + +function run(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_global_export.luac b/lua/tools/tests/fixtures/module_global_export.luac new file mode 100644 index 000000000000..9f38237dfbf5 Binary files /dev/null and b/lua/tools/tests/fixtures/module_global_export.luac differ diff --git a/lua/tools/tests/fixtures/module_global_export_alias.lua b/lua/tools/tests/fixtures/module_global_export_alias.lua new file mode 100644 index 000000000000..7fbacc9df5dc --- /dev/null +++ b/lua/tools/tests/fixtures/module_global_export_alias.lua @@ -0,0 +1,8 @@ +module("sample", package.seeall) + +function primary(value) + return value +end + +alias = primary +unknown_alias = unknown diff --git a/lua/tools/tests/fixtures/module_global_export_alias.luac b/lua/tools/tests/fixtures/module_global_export_alias.luac new file mode 100644 index 000000000000..1a3b002b1443 Binary files /dev/null and b/lua/tools/tests/fixtures/module_global_export_alias.luac differ diff --git a/lua/tools/tests/fixtures/module_global_table_export.lua b/lua/tools/tests/fixtures/module_global_table_export.lua new file mode 100644 index 000000000000..a0dd4d4c4876 --- /dev/null +++ b/lua/tools/tests/fixtures/module_global_table_export.lua @@ -0,0 +1,7 @@ +module("sample") + +handlers = {} + +function handlers.run(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_global_table_export.luac b/lua/tools/tests/fixtures/module_global_table_export.luac new file mode 100644 index 000000000000..4ee28aea2a8f Binary files /dev/null and b/lua/tools/tests/fixtures/module_global_table_export.luac differ diff --git a/lua/tools/tests/fixtures/module_global_table_replacement.lua b/lua/tools/tests/fixtures/module_global_table_replacement.lua new file mode 100644 index 000000000000..611a7af067c6 --- /dev/null +++ b/lua/tools/tests/fixtures/module_global_table_replacement.lua @@ -0,0 +1,18 @@ +module("sample") + +handlers = {} +local stale = handlers +handlers = {} + +function stale.old(value) + return value +end + +function handlers.live(value) + return value +end + +local dynamic_key = ... +handlers[dynamic_key] = function(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_global_table_replacement.luac b/lua/tools/tests/fixtures/module_global_table_replacement.luac new file mode 100644 index 000000000000..ea94c57eb922 Binary files /dev/null and b/lua/tools/tests/fixtures/module_global_table_replacement.luac differ diff --git a/lua/tools/tests/fixtures/module_global_table_seeall_export.lua b/lua/tools/tests/fixtures/module_global_table_seeall_export.lua new file mode 100644 index 000000000000..a4feac44a65f --- /dev/null +++ b/lua/tools/tests/fixtures/module_global_table_seeall_export.lua @@ -0,0 +1,7 @@ +module("sample", package.seeall) + +handlers = {} + +function handlers.run(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_global_table_seeall_export.luac b/lua/tools/tests/fixtures/module_global_table_seeall_export.luac new file mode 100644 index 000000000000..0157652b30de Binary files /dev/null and b/lua/tools/tests/fixtures/module_global_table_seeall_export.luac differ diff --git a/lua/tools/tests/fixtures/module_missing_field_caller.lua b/lua/tools/tests/fixtures/module_missing_field_caller.lua new file mode 100644 index 000000000000..dd48607fb905 --- /dev/null +++ b/lua/tools/tests/fixtures/module_missing_field_caller.lua @@ -0,0 +1,3 @@ +local library = require("samplelib") + +return library.missing("whoami") diff --git a/lua/tools/tests/fixtures/module_missing_field_caller.luac b/lua/tools/tests/fixtures/module_missing_field_caller.luac new file mode 100644 index 000000000000..0a9c2f928295 Binary files /dev/null and b/lua/tools/tests/fixtures/module_missing_field_caller.luac differ diff --git a/lua/tools/tests/fixtures/module_multiple_field_exports.lua b/lua/tools/tests/fixtures/module_multiple_field_exports.lua new file mode 100644 index 000000000000..abaf86cee3ef --- /dev/null +++ b/lua/tools/tests/fixtures/module_multiple_field_exports.lua @@ -0,0 +1,9 @@ +module("samplelib") + +run = function(value) + return value +end + +run = function(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_multiple_field_exports.luac b/lua/tools/tests/fixtures/module_multiple_field_exports.luac new file mode 100644 index 000000000000..914f8fb94524 Binary files /dev/null and b/lua/tools/tests/fixtures/module_multiple_field_exports.luac differ diff --git a/lua/tools/tests/fixtures/module_multiple_same_module_exports.lua b/lua/tools/tests/fixtures/module_multiple_same_module_exports.lua new file mode 100644 index 000000000000..a1cbadbc543d --- /dev/null +++ b/lua/tools/tests/fixtures/module_multiple_same_module_exports.lua @@ -0,0 +1,13 @@ +module("sample") + +function helper(value) + return value +end + +function helper(value) + return value + 1 +end + +function invoke(value) + return helper(value) +end diff --git a/lua/tools/tests/fixtures/module_multiple_same_module_exports.luac b/lua/tools/tests/fixtures/module_multiple_same_module_exports.luac new file mode 100644 index 000000000000..50808dc4499c Binary files /dev/null and b/lua/tools/tests/fixtures/module_multiple_same_module_exports.luac differ diff --git a/lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.lua b/lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.lua new file mode 100644 index 000000000000..b48558b77143 --- /dev/null +++ b/lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.lua @@ -0,0 +1,15 @@ +module("sample") + +handlers = {} + +function handlers.run(value) + return value +end + +function handlers.run(value) + return value + 1 +end + +function invoke(value) + return handlers.run(value) +end diff --git a/lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.luac b/lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.luac new file mode 100644 index 000000000000..fb29236c5191 Binary files /dev/null and b/lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.luac differ diff --git a/lua/tools/tests/fixtures/module_nested_field_caller.lua b/lua/tools/tests/fixtures/module_nested_field_caller.lua new file mode 100644 index 000000000000..36a1f616ab5f --- /dev/null +++ b/lua/tools/tests/fixtures/module_nested_field_caller.lua @@ -0,0 +1,14 @@ +function invoke(value) + local sample = require("sample") + return sample.handlers.run(value) +end + +function missing(value) + local sample = require("sample") + return sample.handlers.missing(value) +end + +function dynamic(value, dynamic_key) + local sample = require("sample") + return sample.handlers[dynamic_key](value) +end diff --git a/lua/tools/tests/fixtures/module_nested_field_caller.luac b/lua/tools/tests/fixtures/module_nested_field_caller.luac new file mode 100644 index 000000000000..01c7b952fa39 Binary files /dev/null and b/lua/tools/tests/fixtures/module_nested_field_caller.luac differ diff --git a/lua/tools/tests/fixtures/module_nested_field_provider.lua b/lua/tools/tests/fixtures/module_nested_field_provider.lua new file mode 100644 index 000000000000..a4feac44a65f --- /dev/null +++ b/lua/tools/tests/fixtures/module_nested_field_provider.lua @@ -0,0 +1,7 @@ +module("sample", package.seeall) + +handlers = {} + +function handlers.run(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_nested_field_provider.luac b/lua/tools/tests/fixtures/module_nested_field_provider.luac new file mode 100644 index 000000000000..8404c480aaf1 Binary files /dev/null and b/lua/tools/tests/fixtures/module_nested_field_provider.luac differ diff --git a/lua/tools/tests/fixtures/module_same_module_export_call.lua b/lua/tools/tests/fixtures/module_same_module_export_call.lua new file mode 100644 index 000000000000..0b629ce36a67 --- /dev/null +++ b/lua/tools/tests/fixtures/module_same_module_export_call.lua @@ -0,0 +1,9 @@ +module("samplelib") + +function helper(value) + return value +end + +function run(value) + return helper(value) +end diff --git a/lua/tools/tests/fixtures/module_same_module_export_call.luac b/lua/tools/tests/fixtures/module_same_module_export_call.luac new file mode 100644 index 000000000000..5e3e676ea53b Binary files /dev/null and b/lua/tools/tests/fixtures/module_same_module_export_call.luac differ diff --git a/lua/tools/tests/fixtures/module_same_module_global_table_call.lua b/lua/tools/tests/fixtures/module_same_module_global_table_call.lua new file mode 100644 index 000000000000..087596a38192 --- /dev/null +++ b/lua/tools/tests/fixtures/module_same_module_global_table_call.lua @@ -0,0 +1,11 @@ +module("sample") + +handlers = {} + +function handlers.run(value) + return value +end + +function invoke(value) + return handlers.run(value) +end diff --git a/lua/tools/tests/fixtures/module_same_module_global_table_call.luac b/lua/tools/tests/fixtures/module_same_module_global_table_call.luac new file mode 100644 index 000000000000..ed289ce75b99 Binary files /dev/null and b/lua/tools/tests/fixtures/module_same_module_global_table_call.luac differ diff --git a/lua/tools/tests/fixtures/module_seeall_export.lua b/lua/tools/tests/fixtures/module_seeall_export.lua new file mode 100644 index 000000000000..2368ff0b7cf5 --- /dev/null +++ b/lua/tools/tests/fixtures/module_seeall_export.lua @@ -0,0 +1,5 @@ +module("sample", package.seeall) + +function run(value) + return value +end diff --git a/lua/tools/tests/fixtures/module_seeall_export.luac b/lua/tools/tests/fixtures/module_seeall_export.luac new file mode 100644 index 000000000000..d16a339d57b6 Binary files /dev/null and b/lua/tools/tests/fixtures/module_seeall_export.luac differ diff --git a/lua/tools/tests/fixtures/numeric_loop.lua.txt b/lua/tools/tests/fixtures/numeric_loop.lua.txt new file mode 100644 index 000000000000..d3db149e2896 --- /dev/null +++ b/lua/tools/tests/fixtures/numeric_loop.lua.txt @@ -0,0 +1,9 @@ +local function last_index(limit, initial) + local result = initial + for index = 1, limit do + result = index + end + return result +end + +return last_index diff --git a/lua/tools/tests/fixtures/numeric_loop.luac b/lua/tools/tests/fixtures/numeric_loop.luac new file mode 100644 index 000000000000..78f820dceffc Binary files /dev/null and b/lua/tools/tests/fixtures/numeric_loop.luac differ diff --git a/lua/tools/tests/fixtures/open_call.lua.txt b/lua/tools/tests/fixtures/open_call.lua.txt new file mode 100644 index 000000000000..d3b332b2dc02 --- /dev/null +++ b/lua/tools/tests/fixtures/open_call.lua.txt @@ -0,0 +1,6 @@ +local function invoke(target, producer, value) + local result = target(producer(value)) + return result +end + +return invoke diff --git a/lua/tools/tests/fixtures/open_call.luac b/lua/tools/tests/fixtures/open_call.luac new file mode 100644 index 000000000000..e7ee07eb0a58 Binary files /dev/null and b/lua/tools/tests/fixtures/open_call.luac differ diff --git a/lua/tools/tests/fixtures/open_vararg_return.lua.txt b/lua/tools/tests/fixtures/open_vararg_return.lua.txt new file mode 100644 index 000000000000..6b30337c0c3d --- /dev/null +++ b/lua/tools/tests/fixtures/open_vararg_return.lua.txt @@ -0,0 +1,5 @@ +local function forward(...) + return ... +end + +return forward diff --git a/lua/tools/tests/fixtures/open_vararg_return.luac b/lua/tools/tests/fixtures/open_vararg_return.luac new file mode 100644 index 000000000000..b2e5eb122c70 Binary files /dev/null and b/lua/tools/tests/fixtures/open_vararg_return.luac differ diff --git a/lua/tools/tests/fixtures/returned_closure.lua b/lua/tools/tests/fixtures/returned_closure.lua new file mode 100644 index 000000000000..d27ad391bb77 --- /dev/null +++ b/lua/tools/tests/fixtures/returned_closure.lua @@ -0,0 +1,5 @@ +local function run(value) + return value +end + +return run diff --git a/lua/tools/tests/fixtures/returned_closure.luac b/lua/tools/tests/fixtures/returned_closure.luac new file mode 100644 index 000000000000..92ecdb18f3a2 Binary files /dev/null and b/lua/tools/tests/fixtures/returned_closure.luac differ diff --git a/lua/tools/tests/fixtures/returned_table_alias.lua b/lua/tools/tests/fixtures/returned_table_alias.lua new file mode 100644 index 000000000000..8acf0e266b43 --- /dev/null +++ b/lua/tools/tests/fixtures/returned_table_alias.lua @@ -0,0 +1,8 @@ +local exported = {} +local alias = exported + +alias.run = function(value) + return value +end + +return exported diff --git a/lua/tools/tests/fixtures/returned_table_alias.luac b/lua/tools/tests/fixtures/returned_table_alias.luac new file mode 100644 index 000000000000..05c219a845f9 Binary files /dev/null and b/lua/tools/tests/fixtures/returned_table_alias.luac differ diff --git a/lua/tools/tests/fixtures/returned_table_replacement.lua b/lua/tools/tests/fixtures/returned_table_replacement.lua new file mode 100644 index 000000000000..cc2ae56334f7 --- /dev/null +++ b/lua/tools/tests/fixtures/returned_table_replacement.lua @@ -0,0 +1,18 @@ +local exported = {} +local alias = exported + +alias.stale = function() + return "stale" +end + +exported = {} +exported.live = function(value) + return value +end + +local key = get_key() +exported[key] = function() + return "dynamic" +end + +return exported diff --git a/lua/tools/tests/fixtures/returned_table_replacement.luac b/lua/tools/tests/fixtures/returned_table_replacement.luac new file mode 100644 index 000000000000..646a5b63a290 Binary files /dev/null and b/lua/tools/tests/fixtures/returned_table_replacement.luac differ diff --git a/lua/tools/tests/fixtures/scalar_range.lua.txt b/lua/tools/tests/fixtures/scalar_range.lua.txt new file mode 100644 index 000000000000..45689b22201e --- /dev/null +++ b/lua/tools/tests/fixtures/scalar_range.lua.txt @@ -0,0 +1,7 @@ +local function scalar_range(condition) + local first, second, third = condition + first = true + return first, second, third +end + +return scalar_range diff --git a/lua/tools/tests/fixtures/scalar_range.luac b/lua/tools/tests/fixtures/scalar_range.luac new file mode 100644 index 000000000000..41a9279282af Binary files /dev/null and b/lua/tools/tests/fixtures/scalar_range.luac differ diff --git a/lua/tools/tests/fixtures/state_access_effects.lua.txt b/lua/tools/tests/fixtures/state_access_effects.lua.txt new file mode 100644 index 000000000000..fa45f2caa8fd --- /dev/null +++ b/lua/tools/tests/fixtures/state_access_effects.lua.txt @@ -0,0 +1,15 @@ +local captured + +local function state_access(object, key, value) + local items = {value, key} + items[key] = value + local selected = items[key] + global_value = selected + selected = global_value + captured = selected + selected = captured + object:consume(selected) + return selected +end + +return state_access diff --git a/lua/tools/tests/fixtures/state_access_effects.luac b/lua/tools/tests/fixtures/state_access_effects.luac new file mode 100644 index 000000000000..b5cfbb1d5403 Binary files /dev/null and b/lua/tools/tests/fixtures/state_access_effects.luac differ diff --git a/lua/tools/tests/fixtures/stripped_literal_require_member_call.lua b/lua/tools/tests/fixtures/stripped_literal_require_member_call.lua new file mode 100644 index 000000000000..0b44243c7883 --- /dev/null +++ b/lua/tools/tests/fixtures/stripped_literal_require_member_call.lua @@ -0,0 +1,3 @@ +local api = require("neutral.api") + +return api.source("payload") diff --git a/lua/tools/tests/fixtures/stripped_literal_require_member_call.luac b/lua/tools/tests/fixtures/stripped_literal_require_member_call.luac new file mode 100644 index 000000000000..f7e6369d9880 Binary files /dev/null and b/lua/tools/tests/fixtures/stripped_literal_require_member_call.luac differ diff --git a/lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.lua b/lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.lua new file mode 100644 index 000000000000..f62bf6388e87 --- /dev/null +++ b/lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.lua @@ -0,0 +1,11 @@ +local api = require("neutral.api") + +local function observe() + return api +end + +local function invoke(value) + return api.source(value) +end + +return observe, invoke("payload") diff --git a/lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.luac b/lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.luac new file mode 100644 index 000000000000..f914ac301a3e Binary files /dev/null and b/lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.luac differ diff --git a/lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.lua b/lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.lua new file mode 100644 index 000000000000..82758bc1d967 --- /dev/null +++ b/lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.lua @@ -0,0 +1,7 @@ +local api = require("neutral.api") + +local function invoke(value) + return api.source(value) +end + +return invoke("payload") diff --git a/lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.luac b/lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.luac new file mode 100644 index 000000000000..fbc92f2a539f Binary files /dev/null and b/lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.luac differ diff --git a/lua/tools/tests/fixtures/stripped_nested_getupval_capture.lua b/lua/tools/tests/fixtures/stripped_nested_getupval_capture.lua new file mode 100644 index 000000000000..20e57e5d81f4 --- /dev/null +++ b/lua/tools/tests/fixtures/stripped_nested_getupval_capture.lua @@ -0,0 +1,11 @@ +local api = require("neutral.api") + +local function outer() + local function inner(value) + return api.source(value) + end + + return inner("payload") +end + +return outer() diff --git a/lua/tools/tests/fixtures/stripped_nested_getupval_capture.luac b/lua/tools/tests/fixtures/stripped_nested_getupval_capture.luac new file mode 100644 index 000000000000..33ac832821e0 Binary files /dev/null and b/lua/tools/tests/fixtures/stripped_nested_getupval_capture.luac differ diff --git a/lua/tools/tests/fixtures/table_alias_replacement.lua.txt b/lua/tools/tests/fixtures/table_alias_replacement.lua.txt new file mode 100644 index 000000000000..9362e840f98e --- /dev/null +++ b/lua/tools/tests/fixtures/table_alias_replacement.lua.txt @@ -0,0 +1,12 @@ +local function select_values(first, second) + local object = {} + object.value = first + local alias = object + local through_alias = alias.value + object = {} + object.value = second + local after_replacement = object.value + return through_alias, after_replacement +end + +return select_values diff --git a/lua/tools/tests/fixtures/table_alias_replacement.luac b/lua/tools/tests/fixtures/table_alias_replacement.luac new file mode 100644 index 000000000000..d08618cc7e6b Binary files /dev/null and b/lua/tools/tests/fixtures/table_alias_replacement.luac differ diff --git a/lua/tools/tests/fixtures/unary_effects.lua.txt b/lua/tools/tests/fixtures/unary_effects.lua.txt new file mode 100644 index 000000000000..82fe02b50ced --- /dev/null +++ b/lua/tools/tests/fixtures/unary_effects.lua.txt @@ -0,0 +1,8 @@ +local function unary_effects(value) + local negative = -value + local logical = not value + local length = #value + return negative, logical, length +end + +return unary_effects diff --git a/lua/tools/tests/fixtures/unary_effects.luac b/lua/tools/tests/fixtures/unary_effects.luac new file mode 100644 index 000000000000..26e83f482bd4 Binary files /dev/null and b/lua/tools/tests/fixtures/unary_effects.luac differ diff --git a/lua/tools/tests/fixtures/upvalue_member_call.lua b/lua/tools/tests/fixtures/upvalue_member_call.lua new file mode 100644 index 000000000000..4203524edccb --- /dev/null +++ b/lua/tools/tests/fixtures/upvalue_member_call.lua @@ -0,0 +1,11 @@ +local provider = {} + +function provider.source() + return "value" +end + +local function invoke() + return provider.source() +end + +return invoke() diff --git a/lua/tools/tests/fixtures/upvalue_member_call.luac b/lua/tools/tests/fixtures/upvalue_member_call.luac new file mode 100644 index 000000000000..ee99243d5478 Binary files /dev/null and b/lua/tools/tests/fixtures/upvalue_member_call.luac differ diff --git a/lua/tools/tests/fixtures/upvalue_member_call_stripped.luac b/lua/tools/tests/fixtures/upvalue_member_call_stripped.luac new file mode 100644 index 000000000000..19c72087a53b Binary files /dev/null and b/lua/tools/tests/fixtures/upvalue_member_call_stripped.luac differ diff --git a/lua/tools/tests/test_corpus_analyzer.py b/lua/tools/tests/test_corpus_analyzer.py new file mode 100644 index 000000000000..381cc83bdaf7 --- /dev/null +++ b/lua/tools/tests/test_corpus_analyzer.py @@ -0,0 +1,5636 @@ +from __future__ import annotations + +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + + +TOOLS_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(TOOLS_DIR)) + +from corpus_analyzer import AcceptedCorpus, CorpusArtifact, analyze_corpus +from lua_bytecode import ( + Chunk, + Constant, + Instruction, + InstructionType, + LoadedArtifact, + Lua51Loader, + Opcode, +) + + +class CorpusAnalyzerPublicApiTests(unittest.TestCase): + def test_structural_identities_are_stable_across_outer_directories(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac" + ) + + with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second: + first_path = Path(first) / "firmware/pkg/input.luac" + second_path = Path(second) / "other-root/pkg/input.luac" + first_path.parent.mkdir(parents=True) + second_path.parent.mkdir(parents=True) + shutil.copyfile(fixture, first_path) + shutil.copyfile(fixture, second_path) + + first_result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path="pkg/input.luac", + loaded_artifact=Lua51Loader(first_path.read_bytes()).load(), + ), + ) + ) + ) + second_result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path="pkg/input.luac", + loaded_artifact=Lua51Loader(second_path.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual(first_result, second_result) + self.assertEqual(first_result.artifact_identities[0].module_path, "pkg/input.luac") + self.assertIn( + ("pkg/input.luac", "root", ""), + { + (item.module_path, item.prototype_id, item.parent_prototype_id) + for item in first_result.prototype_identities + }, + ) + self.assertTrue(first_result.instruction_identities) + self.assertIn( + ("pkg/input.luac", "root.0", "root.0:r0", "entry-register", -1, 0), + { + ( + item.module_path, + item.prototype_id, + item.value_ref, + item.value_kind, + item.pc, + item.slot, + ) + for item in first_result.value_identities + }, + ) + + def test_duplicate_normalized_module_path_is_rejected(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac" + ) + loaded = Lua51Loader(fixture.read_bytes()).load() + + with self.assertRaisesRegex(ValueError, "duplicate normalized module path"): + analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("pkg/input.luac", loaded), + CorpusArtifact("pkg/input.luac", loaded), + ) + ) + ) + + def test_straight_line_parameter_definition_reaches_only_matching_move_read(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/input.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + ( + flow.module_path, + flow.prototype_id, + flow.source_ref, + flow.sink_ref, + flow.kind, + ) + for flow in result.value_flows + } + + self.assertIn( + ( + "pkg/input.luac", + "root.0", + "root.0:r0", + "root.0@pc0:r0", + "reaching-definition", + ), + flows, + ) + self.assertIn( + ( + "pkg/input.luac", + "root.0", + "root.0@pc0:r0", + "root.0@pc0:r2", + "same-instruction-dependence", + ), + flows, + ) + self.assertNotIn( + ( + "pkg/input.luac", + "root.0", + "root.0:r1", + "root.0@pc0:r0", + "reaching-definition", + ), + flows, + ) + + def test_later_write_kills_old_definition_before_sink_read(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/intraprocedural-semantics/bc-kill-overwrite/input.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/input.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + adjacency: dict[str, set[str]] = {} + for flow in result.value_flows: + adjacency.setdefault(flow.source_ref, set()).add(flow.sink_ref) + + def reachable(source: str, sink: str) -> bool: + pending = [source] + seen: set[str] = set() + while pending: + current = pending.pop() + if current == sink: + return True + if current in seen: + continue + seen.add(current) + pending.extend(adjacency.get(current, set()) - seen) + return False + + self.assertTrue(reachable("root@pc4:r2", "root@pc7:r4")) + self.assertFalse(reachable("root@pc3:r2", "root@pc6:r2")) + + def test_conditional_predecessors_merge_definitions_at_join_read(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/branch_merge.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/branch_merge.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + adjacency: dict[str, set[str]] = {} + for flow in result.value_flows: + adjacency.setdefault(flow.source_ref, set()).add(flow.sink_ref) + + def reachable(source: str, sink: str) -> bool: + pending = [source] + seen: set[str] = set() + while pending: + current = pending.pop() + if current == sink: + return True + if current in seen: + continue + seen.add(current) + pending.extend(adjacency.get(current, set()) - seen) + return False + + self.assertTrue(reachable("root.0@pc0:r3", "root.0@pc4:r3")) + self.assertTrue(reachable("root.0@pc3:r3", "root.0@pc4:r3")) + self.assertFalse(reachable("root.0@pc3:r3", "root.0@pc1:r0")) + + def test_return_terminates_control_flow(self) -> None: + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + instructions=[ + Instruction(Opcode.MOVE, InstructionType.ABC, 0, 1, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 0, 1, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/return-terminal.luac", loaded),) + ) + ) + + self.assertEqual( + {(0, 1), (2, 3)}, + { + (edge.source_pc, edge.target_pc) + for edge in result.control_flow_edges + }, + ) + + def test_tailcall_terminates_control_flow_but_call_falls_through(self) -> None: + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + instructions=[ + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 0, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 0, 1, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/tailcall-terminal.luac", loaded),) + ) + ) + + self.assertEqual( + {(0, 1), (2, 3)}, + { + (edge.source_pc, edge.target_pc) + for edge in result.control_flow_edges + }, + ) + + def test_generic_loop_carries_iterator_and_body_definitions(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/generic_loop.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/generic_loop.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + adjacency: dict[str, set[str]] = {} + for flow in result.value_flows: + adjacency.setdefault(flow.source_ref, set()).add(flow.sink_ref) + + def reachable(source: str, sink: str) -> bool: + pending = [source] + seen: set[str] = set() + while pending: + current = pending.pop() + if current == sink: + return True + if current in seen: + continue + seen.add(current) + pending.extend(adjacency.get(current, set()) - seen) + return False + + self.assertTrue(reachable("root.0@pc6:r8", "root.0@pc5:r8")) + self.assertTrue(reachable("root.0:r1", "root.0@pc5:r8")) + self.assertTrue(reachable("root.0@pc5:r3", "root.0@pc8:r3")) + self.assertFalse(reachable("root.0@pc6:r7", "root.0@pc5:r8")) + self.assertFalse(reachable("root.0:r0", "root.0@pc5:r8")) + + def test_numeric_loop_preserves_entry_and_backedge_definitions(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/numeric_loop.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/numeric_loop.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + adjacency: dict[str, set[str]] = {} + for flow in result.value_flows: + adjacency.setdefault(flow.source_ref, set()).add(flow.sink_ref) + + def reachable(source: str, sink: str) -> bool: + pending = [source] + seen: set[str] = set() + while pending: + current = pending.pop() + if current == sink: + return True + if current in seen: + continue + seen.add(current) + pending.extend(adjacency.get(current, set()) - seen) + return False + + self.assertTrue(reachable("root.0@pc0:r2", "root.0@pc7:r2")) + self.assertTrue(reachable("root.0@pc5:r2", "root.0@pc7:r2")) + self.assertFalse(reachable("root.0:r0", "root.0@pc7:r2")) + + def test_open_call_uses_only_producer_proven_argument_slots(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/open_call.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/open_call.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.0@pc3:r4", "root.0@pc4:r4", "reaching-definition"), + flows, + ) + self.assertNotIn( + ("root.0@pc2:r5", "root.0@pc4:r5", "reaching-definition"), + flows, + ) + boundaries = { + (item.prototype_id, item.site_id, item.boundary_kind) + for item in result.boundaries + } + self.assertIn( + ("root.0", "root.0@pc3", "open-call-result-tail"), + boundaries, + ) + self.assertIn( + ("root.0", "root.0@pc4", "open-call-argument-tail"), + boundaries, + ) + + def test_open_call_preserves_fixed_prefix_before_proven_tail(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/mixed_fixed_open_call.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/mixed-fixed-open-call.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.3@pc3:r3", "root.3@pc7:r3", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.3@pc6:r4", "root.3@pc7:r4", "reaching-definition"), + flows, + ) + + def test_open_vararg_return_models_only_the_proven_base_slot(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/open_vararg_return.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/open_vararg_return.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + boundaries = { + (item.prototype_id, item.site_id, item.boundary_kind) + for item in result.boundaries + } + + self.assertIn( + ("root.0@pc0:r1", "root.0@pc1:r1", "reaching-definition"), + flows, + ) + self.assertNotIn( + ("root.0@pc0:r2", "root.0@pc1:r2", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0", "root.0@pc0", "open-vararg-tail"), + boundaries, + ) + self.assertIn( + ("root.0", "root.0@pc1", "open-return-tail"), + boundaries, + ) + + def test_scalar_and_range_writes_reach_only_their_live_reads(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/scalar_range.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/scalar_range.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.0@pc1:r2", "root.0@pc4:r2", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc1:r3", "root.0@pc5:r3", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc2:r1", "root.0@pc3:r1", "reaching-definition"), + flows, + ) + self.assertNotIn( + ("root.0@pc0:r1", "root.0@pc3:r1", "reaching-definition"), + flows, + ) + + def test_arithmetic_and_concat_use_only_register_operands(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/arithmetic_concat.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/arithmetic_concat.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.0@pc0:r0", "root.0@pc0:r3", "same-instruction-dependence"), + flows, + ) + self.assertIn( + ("root.0@pc0:r1", "root.0@pc0:r3", "same-instruction-dependence"), + flows, + ) + self.assertIn( + ("root.0@pc1:r3", "root.0@pc1:r4", "same-instruction-dependence"), + flows, + ) + self.assertFalse( + any(sink == "root.0@pc1:r256" for _, sink, _ in flows) + ) + self.assertIn( + ("root.0@pc5:r6", "root.0@pc5:r0", "same-instruction-dependence"), + flows, + ) + self.assertIn( + ("root.0@pc5:r0", "root.0@pc6:r0", "reaching-definition"), + flows, + ) + self.assertNotIn( + ("root.0:r0", "root.0@pc6:r0", "reaching-definition"), + flows, + ) + + def test_overlapping_concat_preserves_prior_definition_for_tailcall(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/bytecode-model/bc-constants-call/input.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/concat_tailcall.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root@pc6:r4", "root@pc7:r4", "reaching-definition"), + flows, + ) + self.assertIn( + ("root@pc4:r4", "root@pc7:r4", "reaching-definition"), + flows, + ) + self.assertNotIn( + ("root@pc5:r5", "root@pc7:r5", "reaching-definition"), + flows, + ) + + def test_unary_effects_depend_only_on_their_input_slot(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/unary_effects.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/unary_effects.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + observed = { + (flow.source_ref, flow.sink_ref) + for flow in result.value_flows + if flow.kind == "same-instruction-dependence" + and flow.source_ref + in {"root.0@pc0:r0", "root.0@pc1:r0", "root.0@pc2:r0"} + } + + self.assertEqual( + { + ("root.0@pc0:r0", "root.0@pc0:r1"), + ("root.0@pc1:r0", "root.0@pc1:r2"), + ("root.0@pc2:r0", "root.0@pc2:r3"), + }, + observed, + ) + + def test_conditional_effects_merge_only_legitimate_definitions(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/conditional_effects.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/conditional_effects.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.0@pc0:r0", "root.0@pc0:r3", "same-instruction-dependence"), + flows, + ) + self.assertIn( + ("root.0@pc0:r3", "root.0@pc15:r3", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc2:r3", "root.0@pc15:r3", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0:r0", "root.0@pc3:r0", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0:r1", "root.0@pc11:r1", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc6:r4", "root.0@pc16:r4", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc10:r5", "root.0@pc17:r5", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc14:r6", "root.0@pc18:r6", "reaching-definition"), + flows, + ) + self.assertNotIn( + ("root.0:r2", "root.0@pc3:r0", "reaching-definition"), + flows, + ) + + def test_state_access_opcodes_have_precise_register_effects(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/state_access_effects.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/state_access_effects.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.0@pc0:r3", "root.0@pc3:r3", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc1:r4", "root.0@pc3:r4", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc2:r5", "root.0@pc3:r5", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0:r1", "root.0@pc4:r1", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc5:r3", "root.0@pc5:r4", "same-instruction-dependence"), + flows, + ) + self.assertNotIn( + ("root.0@pc5:r1", "root.0@pc5:r4", "same-instruction-dependence"), + flows, + ) + self.assertIn( + ("root.0@pc5:r4", "root.0@pc6:r4", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc7:r4", "root.0@pc8:r4", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc9:r4", "root.0@pc13:r4", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc10:r0", "root.0@pc10:r5", "same-instruction-dependence"), + flows, + ) + self.assertIn( + ("root.0@pc10:r0", "root.0@pc10:r6", "same-instruction-dependence"), + flows, + ) + self.assertFalse( + any(sink == "root.0@pc10:r257" for _, sink, _ in flows) + ) + self.assertIn( + ("root.0@pc10:r6", "root.0@pc12:r6", "reaching-definition"), + flows, + ) + self.assertIn( + ("root.0@pc11:r7", "root.0@pc12:r7", "reaching-definition"), + flows, + ) + + def test_fixed_vararg_writes_exactly_the_declared_result_slots(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/fixed_vararg.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/fixed_vararg.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + for slot in (1, 2, 3): + self.assertIn( + ( + f"root.0@pc0:r{slot}", + f"root.0@pc{slot}:r{slot}", + "reaching-definition", + ), + flows, + ) + self.assertFalse( + any(source == "root.0@pc0:r4" for source, _, _ in flows) + ) + + def test_close_preserves_ordinary_register_definitions(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/close_effect.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/close_effect.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + } + + self.assertIn( + ("root.0@pc6:r1", "root.0@pc8:r1", "reaching-definition"), + flows, + ) + self.assertFalse( + any( + source.startswith("root.0@pc7:") + or sink.startswith("root.0@pc7:") + for source, sink, _ in flows + ) + ) + + def test_precise_table_field_flow_rejects_dynamic_and_missing_keys(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/intraprocedural-semantics" + ) + positive_path = fixture_root / "bc-table-global-upvalue/input.luac" + negative_path = fixture_root / "table-dynamic-key-negative/input.luac" + + positive = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "positive/input.luac", + Lua51Loader(positive_path.read_bytes()).load(), + ), + ) + ) + ) + negative = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "negative/input.luac", + Lua51Loader(negative_path.read_bytes()).load(), + ), + ) + ) + ) + self.assertEqual( + { + ( + "positive/input.luac", + "root.0", + "root.0@pc0:r1", + "key", + "root.0@pc1:r0", + "root.0@pc3:r3", + "bytecode-only,precise-table-field", + ) + }, + { + ( + flow.module_path, + flow.prototype_id, + flow.table_ref, + flow.field_name, + flow.write_ref, + flow.read_ref, + flow.provenance, + ) + for flow in positive.table_field_flows + }, + ) + self.assertEqual((), negative.table_field_flows) + + def test_table_alias_preserves_identity_and_replacement_drops_old_fields(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/table_alias_replacement.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/table_alias_replacement.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + flows = { + (flow.table_ref, flow.field_name, flow.write_ref, flow.read_ref) + for flow in result.table_field_flows + } + + self.assertIn( + ("root.0@pc0:r2", "value", "root.0@pc1:r0", "root.0@pc3:r4"), + flows, + ) + self.assertIn( + ("root.0@pc4:r5", "value", "root.0@pc6:r1", "root.0@pc7:r5"), + flows, + ) + self.assertNotIn( + ("root.0@pc0:r2", "value", "root.0@pc1:r0", "root.0@pc7:r5"), + flows, + ) + + def test_static_table_field_definitions_join_across_control_flow(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/experimental/query-tests/rules-sanitizer-report" + / "table-field-sanitizer-overwrite" + ) + expected_writes_by_fixture = { + "same-field.luac": { + ("root@pc11:r1", "root@pc18:r2"), + ("root@pc16:r1", "root@pc18:r2"), + }, + "optional-branch.luac": { + ("root@pc3:r1", "root@pc13:r2"), + ("root@pc11:r1", "root@pc13:r2"), + }, + "unrelated-field.luac": { + ("root@pc3:r1", "root@pc18:r2"), + }, + "dynamic-key.luac": { + ("root@pc3:r1", "root@pc15:r2"), + }, + } + + for fixture_name, expected_writes in expected_writes_by_fixture.items(): + with self.subTest(fixture=fixture_name): + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + fixture_name, + Lua51Loader( + (fixture_root / fixture_name).read_bytes() + ).load(), + ), + ) + ) + ) + final_read_pc = max( + int(flow.read_ref.split("@pc", 1)[1].split(":", 1)[0]) + for flow in result.table_field_flows + if flow.field_name == "command" + ) + self.assertEqual( + expected_writes, + { + (flow.write_ref, flow.read_ref) + for flow in result.table_field_flows + if flow.field_name == "command" + and f"@pc{final_read_pc}:" in flow.read_ref + }, + ) + + def test_table_object_values_reach_only_reads_of_the_current_object(self) -> None: + fixtures = Path(__file__).resolve().parent / "fixtures" + state_result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/state_access_effects.luac", + Lua51Loader((fixtures / "state_access_effects.luac").read_bytes()).load(), + ), + ) + ) + ) + replacement_result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/table_alias_replacement.luac", + Lua51Loader((fixtures / "table_alias_replacement.luac").read_bytes()).load(), + ), + ) + ) + ) + state_flows = { + (flow.source_ref, flow.sink_ref) + for flow in state_result.value_flows + if flow.kind == "table-object-dependence" + } + replacement_flows = { + (flow.source_ref, flow.sink_ref) + for flow in replacement_result.value_flows + if flow.kind == "table-object-dependence" + } + + self.assertIn(("root.0@pc3:r4", "root.0@pc5:r3"), state_flows) + self.assertIn(("root.0@pc3:r5", "root.0@pc5:r3"), state_flows) + self.assertIn(("root.0@pc4:r2", "root.0@pc5:r3"), state_flows) + self.assertNotIn(("root.0@pc3:r3", "root.0@pc5:r3"), state_flows) + self.assertIn( + ("root.0@pc1:r0", "root.0@pc2:r2"), + replacement_flows, + ) + self.assertIn( + ("root.0@pc1:r0", "root.0@pc3:r3"), + replacement_flows, + ) + self.assertIn( + ("root.0@pc6:r1", "root.0@pc7:r2"), + replacement_flows, + ) + self.assertNotIn( + ("root.0@pc1:r0", "root.0@pc7:r2"), + replacement_flows, + ) + + def test_call_result_table_carries_written_value_to_later_read(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/call_result_table.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/call_result_table.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + object_flows = { + (flow.source_ref, flow.sink_ref) + for flow in result.value_flows + if flow.kind == "table-object-dependence" + } + + self.assertIn(("root.1@pc2:r0", "root.1@pc3:r2"), object_flows) + self.assertEqual((), result.table_field_flows) + + def test_precise_global_writes_reach_only_matching_later_reads(self) -> None: + fixtures = Path(__file__).resolve().parent / "fixtures" + positive = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/state_access_effects.luac", + Lua51Loader((fixtures / "state_access_effects.luac").read_bytes()).load(), + ), + ) + ) + ) + negative_path = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/intraprocedural-semantics" + / "global-dynamic-environment-negative/input.luac" + ) + negative = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/dynamic-env.luac", + Lua51Loader(negative_path.read_bytes()).load(), + ), + ) + ) + ) + replacement = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/global-replacement.luac", + LoadedArtifact( + chunk=Chunk( + max_stack=3, + instructions=[ + Instruction(Opcode.SETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.SETGLOBAL, InstructionType.ABx, 1, 0), + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 2, 0), + ], + constants=[Constant("string", "global_value")], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "pkg/state_access_effects.luac", + "root.0", + "global_value", + "root.0@pc6:r4", + "root.0@pc7:r4", + "root.0@pc6:r4", + "bytecode-only,precise-global-state", + ) + }, + { + ( + flow.module_path, + flow.prototype_id, + flow.global_name, + flow.write_ref, + flow.read_ref, + flow.value_ref, + flow.provenance, + ) + for flow in positive.global_flows + }, + ) + self.assertEqual((), negative.global_flows) + self.assertEqual( + {("root@pc1:r1", "root@pc2:r2")}, + {(flow.write_ref, flow.read_ref) for flow in replacement.global_flows}, + ) + + def test_upvalue_rows_preserve_capture_read_and_first_write_evidence(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/intraprocedural-semantics" + ) + positive = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "positive/input.luac", + Lua51Loader( + (fixture_root / "bc-table-global-upvalue/input.luac").read_bytes() + ).load(), + ), + ) + ) + ) + mutation = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "mutation/input.luac", + Lua51Loader( + (fixture_root / "upvalue-mutation-negative/input.luac").read_bytes() + ).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/input.luac", + "root.0", + "root.0:u0", + "root@pc2:r0", + "root.0@pc4:r4", + "", + "bytecode-only,derived-upvalue-flow", + ) + }, + { + ( + flow.module_path, + flow.prototype_id, + flow.upvalue_id, + flow.capture_ref, + flow.read_ref, + flow.write_ref, + flow.provenance, + ) + for flow in positive.upvalue_flows + }, + ) + self.assertEqual( + { + ( + "root.0:u0", + "root@pc2:r0", + "root.0@pc0:r0", + "root.0@pc2:r1", + ), + ( + "root.0:u0", + "root@pc2:r0", + "root.0@pc3:r1", + "root.0@pc2:r1", + ), + }, + { + ( + flow.upvalue_id, + flow.capture_ref, + flow.read_ref, + flow.write_ref, + ) + for flow in mutation.upvalue_flows + }, + ) + self.assertFalse( + any( + flow.capture_ref == "root.0@pc0:r0" + and flow.read_ref == "root.0@pc3:r1" + for flow in mutation.upvalue_flows + ) + ) + + def test_malformed_upvalue_bindings_emit_boundaries_without_guessing(self) -> None: + def loaded_with_binding(binding: Instruction | None) -> LoadedArtifact: + child = Chunk( + num_upvalues=1, + max_stack=1, + instructions=[ + Instruction(Opcode.GETUPVAL, InstructionType.ABC, 0, 0, 0), + ], + ) + instructions = [ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + ] + if binding is not None: + instructions.append(binding) + return LoadedArtifact( + chunk=Chunk( + max_stack=1, + instructions=instructions, + constants=[Constant("string", "unused")], + protos=[child], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "invalid/input.luac", + loaded_with_binding( + Instruction(Opcode.LOADK, InstructionType.ABx, 0, 0) + ), + ), + CorpusArtifact( + "missing/input.luac", + loaded_with_binding(None), + ), + ) + ) + ) + + self.assertEqual((), result.upvalue_flows) + self.assertEqual( + { + ( + "invalid/input.luac", + "root", + "root@pc0:u0", + "malformed-upvalue-capture", + "upvalue 0 binding at root@pc1 must use MOVE or GETUPVAL", + "bytecode-only,upvalue-capture-boundary", + ), + ( + "missing/input.luac", + "root", + "root@pc0:u0", + "malformed-upvalue-capture", + "upvalue 0 binding after root@pc0 is missing", + "bytecode-only,upvalue-capture-boundary", + ), + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "malformed-upvalue-capture" + }, + ) + + def test_named_upvalue_member_call_has_public_resolved_name(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/upvalue_member_call.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/upvalue-member.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ("root.1", "root.1@pc2", "provider.source"), + { + ( + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.resolved_name, + ) + for resolution in result.call_resolutions + }, + ) + + def test_stripped_captured_table_member_resolves_target_without_name(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/upvalue_member_call_stripped.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/upvalue-member-stripped.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "root.1", + "root.1@pc2", + "", + "closure-table-field", + "root.0", + ) + }, + { + ( + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + if resolution.caller_prototype_id == "root.1" + }, + ) + self.assertFalse( + any( + boundary.prototype_id == "root.1" + and boundary.site_id == "root.1@pc2" + and boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_stripped_upvalue_member_call_recovers_literal_require_capture(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/stripped_literal_require_upvalue_member.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/stripped-literal-require-upvalue.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ("root.0", "neutral.api.source"), + { + (resolution.caller_prototype_id, resolution.resolved_name) + for resolution in result.call_resolutions + }, + ) + self.assertFalse( + any( + boundary.prototype_id == "root.0" and + boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_sibling_closure_binding_does_not_clobber_capture_name(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/stripped_literal_require_sibling_capture.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/stripped-literal-require-siblings.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ("root.1", "neutral.api.source"), + { + (resolution.caller_prototype_id, resolution.resolved_name) + for resolution in result.call_resolutions + }, + ) + + def test_literal_require_result_member_call_has_qualified_name(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/stripped_literal_require_member_call.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/stripped-literal-require-member.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ( + "neutral.api.source", + "derived-reaching-definition-call-target", + "", + ), + { + ( + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + }, + ) + + def test_nested_getupval_binding_preserves_captured_call_name(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/stripped_nested_getupval_capture.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "neutral/stripped-nested-getupval-capture.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ( + "root.0.0", + "neutral.api.source", + "derived-reaching-definition-call-target", + "", + ), + { + ( + resolution.caller_prototype_id, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + }, + ) + self.assertNotIn( + ("root.0.0", "root.0.0"), + { + ( + resolution.caller_prototype_id, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + }, + ) + + def test_direct_closure_call_resolves_only_the_reaching_target(self) -> None: + child = Chunk(max_stack=1, instructions=[]) + positive = LoadedArtifact( + chunk=Chunk( + max_stack=4, + protos=[child, child], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 2, 0), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 3, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 2, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + parameter_derived = LoadedArtifact( + chunk=Chunk( + num_params=1, + max_stack=1, + instructions=[ + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("positive/direct.luac", positive), + CorpusArtifact("negative/parameter.luac", parameter_derived), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/direct.luac", + "root", + "root@pc2", + "root@pc2:r2", + "", + "direct-closure", + "positive/direct.luac", + "root.0", + "bytecode-only,direct-closure-target", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_closure_call_target_propagates_through_move_only(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/intraprocedural-semantics" + / "bc-call-candidate-unresolved/input.luac" + ) + non_move_transform = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[Chunk(max_stack=1, instructions=[])], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CONCAT, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 1, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/closure-move.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + CorpusArtifact("negative/non-move.luac", non_move_transform), + ) + ) + ) + + self.assertEqual( + { + ( + "pkg/closure-move.luac", + "root", + "root@pc5", + "root@pc5:r2", + "invoke", + "closure-move", + "pkg/closure-move.luac", + "root.1", + "bytecode-only,closure-move-target", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_parameter_derived_call_emits_boundary_without_resolution(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/intraprocedural-semantics" + / "bc-call-candidate-unresolved/input.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "bc-call-candidate-unresolved/input.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + {("root@pc5", "root.1")}, + { + (resolution.callsite_id, resolution.target_prototype_id) + for resolution in result.call_resolutions + }, + ) + self.assertEqual( + { + ( + "bc-call-candidate-unresolved/input.luac", + "root.1", + "root.1@pc2", + "unresolved-call-target", + "param-derived", + "bytecode-only,call-resolution-boundary", + ) + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-call-target" + }, + ) + + def test_multiple_closure_targets_emit_boundary_without_selection(self) -> None: + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[ + Chunk(max_stack=1, instructions=[]), + Chunk(max_stack=1, instructions=[]), + ], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADBOOL, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.TEST, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.JMP, InstructionType.AsBx, 0, 1), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("ambiguous/call.luac", loaded),) + ) + ) + + self.assertEqual( + {"root@pc0:r0", "root@pc4:r0"}, + { + flow.source_ref + for flow in result.value_flows + if flow.sink_ref == "root@pc5:r0" + and flow.kind == "reaching-definition" + }, + ) + self.assertEqual((), result.call_resolutions) + self.assertEqual( + { + ( + "ambiguous/call.luac", + "root", + "root@pc5", + "unresolved-call-target", + "multiple-candidates", + "bytecode-only,call-resolution-boundary", + ) + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-call-target" + }, + ) + + def test_multiple_symbolic_names_emit_boundary_without_selection(self) -> None: + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + constants=[ + Constant("string", "os"), + Constant("string", "execute"), + Constant("string", "io"), + Constant("string", "popen"), + ], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.GETTABLE, InstructionType.ABC, 0, 0, 257), + Instruction(Opcode.LOADBOOL, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.TEST, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.JMP, InstructionType.AsBx, 0, 2), + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 2), + Instruction(Opcode.GETTABLE, InstructionType.ABC, 0, 0, 259), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("ambiguous/names.luac", loaded),) + ) + ) + + self.assertEqual( + {"root@pc1:r0", "root@pc6:r0"}, + { + flow.source_ref + for flow in result.value_flows + if flow.sink_ref == "root@pc7:r0" + and flow.kind == "reaching-definition" + }, + ) + self.assertEqual((), result.call_resolutions) + self.assertEqual( + { + ( + "ambiguous/names.luac", + "root", + "root@pc7", + "unresolved-call-target", + "multiple-candidates", + "bytecode-only,call-resolution-boundary", + ) + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-call-target" + }, + ) + + def test_dynamic_call_emits_no_proven_target_boundary(self) -> None: + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + constants=[Constant("string", "handler")], + instructions=[ + Instruction(Opcode.NEWTABLE, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.GETTABLE, InstructionType.ABC, 0, 0, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("dynamic/call.luac", loaded),) + ) + ) + + self.assertIn( + ("root@pc2:r0", "root@pc3:r0", "reaching-definition"), + { + (flow.source_ref, flow.sink_ref, flow.kind) + for flow in result.value_flows + }, + ) + self.assertEqual((), result.call_resolutions) + self.assertEqual( + { + ( + "dynamic/call.luac", + "root", + "root@pc3", + "unresolved-call-target", + "no-proven-target", + "bytecode-only,call-resolution-boundary", + ) + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-call-target" + }, + ) + + def test_same_callsite_text_remains_scoped_to_each_module(self) -> None: + resolved = LoadedArtifact( + chunk=Chunk( + max_stack=1, + constants=[Constant("string", "handler")], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + unresolved = LoadedArtifact( + chunk=Chunk( + max_stack=1, + instructions=[ + Instruction(Opcode.NEWTABLE, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("a/resolved.luac", resolved), + CorpusArtifact("b/unresolved.luac", unresolved), + ) + ) + ) + + self.assertEqual( + {("a/resolved.luac", "root", "root@pc1", "handler")}, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.resolved_name, + ) + for resolution in result.call_resolutions + }, + ) + self.assertEqual( + {("b/unresolved.luac", "root", "root@pc1", "no-proven-target")}, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.reason, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-call-target" + }, + ) + + def test_exact_global_call_name_reaches_across_argument_setup(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "cross-module-webcmd-popen/controller.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "pkg/controller.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ( + "pkg/controller.luac", + "root", + "root@pc10", + "require", + "derived-reaching-definition-call-target", + "bytecode-only,derived-call-target,reaching-definition", + ), + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.resolved_name, + resolution.resolution_kind, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_literal_require_requires_reaching_string_argument(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + ) + literal_path = "cross-module-webcmd-popen/controller.luac" + dynamic_path = ( + "ambiguous-unresolved-dynamic-module-negative/controller.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=tuple( + CorpusArtifact( + module_path, + Lua51Loader((fixture_root / module_path).read_bytes()).load(), + ) + for module_path in (literal_path, dynamic_path) + ) + ) + ) + + self.assertEqual( + { + ( + literal_path, + "root", + "root@pc10", + "mtkwifi", + "root@pc10:r1", + "bytecode-only,derived-literal-require", + ) + }, + { + ( + require.caller_module_path, + require.caller_prototype_id, + require.callsite_id, + require.require_string, + require.argument_ref, + require.provenance, + ) + for require in result.literal_requires + }, + ) + self.assertEqual( + { + ( + dynamic_path, + "root.0", + "root.0@pc2", + "unresolved-literal-require", + "dynamic-argument", + "bytecode-only,literal-require-boundary", + ) + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-literal-require" + }, + ) + + def test_conflicting_literal_require_does_not_select_branch(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/conflicting_literal_require.luac" + ) + module_path = "neutral/conflicting_literal_require.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + {"root@pc3:r0", "root@pc5:r0"}, + { + flow.source_ref + for flow in result.value_flows + if flow.sink_ref == "root@pc7:r0" + and flow.kind == "reaching-definition" + }, + ) + self.assertIn( + (module_path, "root@pc8", "require"), + { + ( + resolution.caller_module_path, + resolution.callsite_id, + resolution.resolved_name, + ) + for resolution in result.call_resolutions + }, + ) + self.assertEqual((), result.literal_requires) + self.assertEqual( + { + ( + module_path, + "root", + "root@pc8", + "unresolved-literal-require", + "dynamic-argument", + "bytecode-only,literal-require-boundary", + ) + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + boundary.reason, + boundary.provenance, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-literal-require" + }, + ) + + def test_literal_require_resolves_one_matching_module_path(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + ) + caller_path = "cross-module-webcmd-popen/controller.luac" + target_path = "cross-module-webcmd-popen/mtkwifi.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=tuple( + CorpusArtifact( + module_path, + Lua51Loader((fixture_root / module_path).read_bytes()).load(), + ) + for module_path in (caller_path, target_path) + ) + ) + ) + + self.assertEqual( + { + ( + caller_path, + "root@pc10", + "mtkwifi", + "matched", + target_path, + "", + "bytecode-only,literal-require,module-path", + ) + }, + { + ( + resolution.caller_module_path, + resolution.callsite_id, + resolution.require_string, + resolution.status, + resolution.target_module_path, + resolution.reason, + resolution.provenance, + ) + for resolution in result.module_resolutions + }, + ) + + def test_nonunique_module_paths_remain_explicitly_unresolved(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + ) + caller_fixture = ( + fixture_root + / "cross-module-webcmd-popen/controller.luac" + ) + target_fixture = ( + fixture_root + / "cross-module-webcmd-popen/mtkwifi.luac" + ) + missing_fixture = ( + fixture_root + / "ambiguous-unresolved-dynamic-module-negative/missing.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "caller/controller.luac", + Lua51Loader(caller_fixture.read_bytes()).load(), + ), + CorpusArtifact( + "left/mtkwifi.luac", + Lua51Loader(target_fixture.read_bytes()).load(), + ), + CorpusArtifact( + "right/mtkwifi.luac", + Lua51Loader(target_fixture.read_bytes()).load(), + ), + CorpusArtifact( + "missing/requester.luac", + Lua51Loader(missing_fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "caller/controller.luac", + "root@pc10", + "mtkwifi", + "ambiguous", + "", + "ambiguous-module-path-candidates", + ), + ( + "missing/requester.luac", + "root.0@pc2", + "missing.module", + "unresolved", + "", + "no-module-path-candidate", + ), + }, + { + ( + resolution.caller_module_path, + resolution.callsite_id, + resolution.require_string, + resolution.status, + resolution.target_module_path, + resolution.reason, + ) + for resolution in result.module_resolutions + }, + ) + self.assertEqual( + { + ( + "caller/controller.luac", + "root", + "root@pc10", + "ambiguous-module-path-candidates", + ), + ( + "missing/requester.luac", + "root.0", + "root.0@pc2", + "no-module-path-candidate", + ), + }, + { + ( + boundary.module_path, + boundary.prototype_id, + boundary.site_id, + boundary.reason, + ) + for boundary in result.boundaries + if boundary.boundary_kind == "unresolved-module-require" + and boundary.provenance + == "bytecode-only,module-resolution-boundary" + }, + ) + + def test_module_resolution_uses_suffix_names_across_path_renames(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "cross-module-webcmd-popen" + ) + caller = Lua51Loader((fixture_root / "controller.luac").read_bytes()).load() + target = Lua51Loader((fixture_root / "mtkwifi.luac").read_bytes()).load() + + def resolved_target(caller_path: str, target_path: str) -> str: + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact(caller_path, caller), + CorpusArtifact(target_path, target), + ) + ) + ) + self.assertEqual("matched", result.module_resolutions[0].status) + return result.module_resolutions[0].target_module_path + + self.assertEqual( + "tree/mtkwifi.lua", + resolved_target("tree/controller.lua", "tree/mtkwifi.lua"), + ) + self.assertEqual( + "renamed/deep/mtkwifi.luac", + resolved_target( + "renamed/deep/controller.luac", + "renamed/deep/mtkwifi.luac", + ), + ) + + def test_module_resolution_excludes_self_and_package_init_guess(self) -> None: + fixture_root = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + ) + controller = Lua51Loader( + ( + fixture_root + / "cross-module-webcmd-popen/controller.luac" + ).read_bytes() + ).load() + missing = Lua51Loader( + ( + fixture_root + / "ambiguous-unresolved-dynamic-module-negative/missing.luac" + ).read_bytes() + ).load() + target = Lua51Loader( + ( + fixture_root + / "cross-module-webcmd-popen/mtkwifi.luac" + ).read_bytes() + ).load() + + self_result = analyze_corpus( + AcceptedCorpus(artifacts=(CorpusArtifact("mtkwifi.luac", controller),)) + ) + init_result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("caller/requester.luac", missing), + CorpusArtifact("missing/module/init.lua", target), + ) + ) + ) + + self.assertEqual( + ("unresolved", "", "no-module-path-candidate"), + ( + self_result.module_resolutions[0].status, + self_result.module_resolutions[0].target_module_path, + self_result.module_resolutions[0].reason, + ), + ) + self.assertEqual( + ("missing.module", "unresolved", "", "no-module-path-candidate"), + ( + init_result.module_resolutions[0].require_string, + init_result.module_resolutions[0].status, + init_result.module_resolutions[0].target_module_path, + init_result.module_resolutions[0].reason, + ), + ) + self.assertEqual( + {"root@pc10"}, + { + boundary.site_id + for boundary in self_result.boundaries + if boundary.boundary_kind == "unresolved-module-require" + }, + ) + self.assertEqual( + {"root.0@pc2"}, + { + boundary.site_id + for boundary in init_result.boundaries + if boundary.boundary_kind == "unresolved-module-require" + }, + ) + + def test_returned_table_closure_field_is_a_module_export(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call/samplelib.luac" + ) + module_path = "pkg/library.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "returned-table-field", + "run", + "root@pc1:r1", + "root.0", + "bytecode-only,module-return-table", + ) + }, + { + ( + export.module_path, + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_returned_table_alias_preserves_closure_field_export(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/returned_table_alias.luac" + ) + module_path = "neutral/returned_table_alias.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "returned-table-field", + "run", + "root@pc2:r2", + "root.0", + ) + }, + { + ( + export.module_path, + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + ) + for export in result.module_exports + }, + ) + + def test_returned_table_export_rejects_stale_alias_and_dynamic_key(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/returned_table_replacement.luac" + ) + module_path = "neutral/returned_table_replacement.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "returned-table-field", + "live", + "root@pc6:r2", + "root.1", + ) + }, + { + ( + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + ) + for export in result.module_exports + }, + ) + + def test_direct_returned_closure_is_a_module_export(self) -> None: + fixture = Path(__file__).resolve().parent / "fixtures/returned_closure.luac" + module_path = "neutral/returned_closure.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "returned-closure", + "", + "root@pc0:r0", + "root.0", + "bytecode-only,module-return-closure", + ) + }, + { + ( + export.module_path, + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_validated_module_call_exposes_global_closure_export(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_global_export.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "sample.luac", + "module-global", + "run", + "root@pc3:r0", + "root.0", + "bytecode-only,module-global-export,module-call", + ) + }, + { + ( + export.module_path, + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_module_name_must_match_path_to_expose_global_export(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_global_export.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "other/path.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertTrue( + any( + resolution.caller_module_path == "other/path.luac" + and resolution.callsite_id == "root@pc2" + and resolution.resolved_name == "module" + for resolution in result.call_resolutions + ) + ) + self.assertEqual((), result.module_exports) + + def test_package_seeall_is_preserved_as_module_export_provenance(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_seeall_export.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "module-global", + "run", + "root@pc5:r0", + "root.0", + "bytecode-only,module-global-export,module-call", + ), + ( + "module-global", + "run", + "root@pc5:r0", + "root.0", + "bytecode-only,module-global-export,module-seeall", + ), + }, + { + ( + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_package_seeall_exports_static_global_closure_alias(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_global_export_alias.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "primary", + "root@pc5:r0", + "root.0", + "bytecode-only,module-global-export,module-call", + ), + ( + "primary", + "root@pc5:r0", + "root.0", + "bytecode-only,module-global-export,module-seeall", + ), + ( + "alias", + "root@pc5:r0", + "root.0", + "bytecode-only,module-global-export,module-seeall", + ), + }, + { + ( + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_module_global_table_closure_field_is_an_export(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_global_table_export.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "sample.luac", + "module-global-table-field", + "handlers.run", + "root@pc6:r1", + "root.0", + "bytecode-only,module-global-table-field-export,module-call", + ) + }, + { + ( + export.module_path, + export.export_kind, + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_seeall_global_table_export_preserves_both_provenances(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_global_table_seeall_export.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "handlers.run", + "root@pc8:r1", + "root.0", + "bytecode-only,module-global-table-field-export,module-call", + ), + ( + "handlers.run", + "root@pc8:r1", + "root.0", + "bytecode-only,module-global-table-field-export,module-seeall", + ), + }, + { + ( + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_global_table_replacement_rejects_stale_and_dynamic_exports(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_global_table_replacement.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "handlers.live", + "root@pc11:r2", + "root.1", + "bytecode-only,module-global-table-field-export,module-call", + ) + }, + { + ( + export.field_name, + export.value_ref, + export.target_prototype_id, + export.provenance, + ) + for export in result.module_exports + }, + ) + + def test_required_returned_field_resolves_cross_module_call_target(self) -> None: + fixture_dir = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "app/controller.luac", + Lua51Loader((fixture_dir / "controller.luac").read_bytes()).load(), + ), + CorpusArtifact( + "lib/samplelib.luac", + Lua51Loader((fixture_dir / "samplelib.luac").read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "app/controller.luac", + "root.0", + "root.0@pc10", + "root.0@pc10:r2", + "samplelib.run", + "module-field-export", + "lib/samplelib.luac", + "root.0", + "bytecode-only,literal-require,module-return-table", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.callsite_id == "root.0@pc10" + }, + ) + self.assertFalse( + any( + boundary.module_path == "app/controller.luac" + and boundary.site_id == "root.0@pc10" + and boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_required_field_target_flows_through_closure_upvalue(self) -> None: + fixture_dir = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "cross-module-webcmd-popen" + ) + controller_path = "cross-module-webcmd-popen/controller.luac" + target_path = "cross-module-webcmd-popen/mtkwifi.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + controller_path, + Lua51Loader((fixture_dir / "controller.luac").read_bytes()).load(), + ), + CorpusArtifact( + target_path, + Lua51Loader((fixture_dir / "mtkwifi.luac").read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + controller_path, + "root.1", + "root.1@pc8", + "root.1@pc8:r1", + "mtkwifi.func_unknow_0_12", + "module-field-export", + target_path, + "root.1", + "bytecode-only,literal-require,module-return-table", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.caller_module_path == controller_path + and resolution.callsite_id == "root.1@pc8" + }, + ) + self.assertFalse( + any( + boundary.module_path == controller_path + and boundary.site_id == "root.1@pc8" + and boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_fixed_argument_flows_to_fixed_parameter_of_proven_target(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "same-module-formvalue-execute/input.luac" + ) + module_path = "same-module-formvalue-execute/input.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "root", + "root@pc18", + module_path, + "root.3", + "root@pc18:r4", + "root.3:r0", + "argument-to-parameter", + 0, + "bytecode-only,resolved-call,fixed-argument", + ) + }, + { + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "argument-to-parameter" + }, + ) + + def test_structural_tailcall_result_flows_to_resolved_outer_call_result( + self, + ) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "same-module-formvalue-execute/input.luac" + ) + module_path = "same-module-formvalue-execute/input.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "root", + "root@pc15", + module_path, + "root.2", + "root.2@pc4:r0", + "root@pc15:r2", + "return-to-result", + 0, + "bytecode-only,resolved-call,structural-tailcall-result", + ) + }, + { + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.callsite_id == "root@pc15" + and flow.source_ref == "root.2@pc4:r0" + }, + ) + + def test_structural_tailcall_argument_flows_to_resolved_outer_call_result( + self, + ) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "same-module-formvalue-execute/input.luac" + ) + module_path = "same-module-formvalue-execute/input.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "root", + "root@pc15", + module_path, + "root.2", + "root.2@pc4:r1", + "root@pc15:r2", + "return-to-result", + 0, + "bytecode-only,resolved-call,structural-tailcall-argument", + ) + }, + { + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.callsite_id == "root@pc15" + and flow.source_ref == "root.2@pc4:r1" + }, + ) + + def test_structural_open_tailcall_uses_producer_proven_argument_reads( + self, + ) -> None: + producer = Chunk( + num_params=0, + is_vararg=False, + max_stack=1, + instructions=[], + ) + wrapper = Chunk( + num_params=0, + is_vararg=False, + max_stack=3, + protos=[producer], + constants=[Constant("string", "external")], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 1, 1, 0), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 0, 0), + ], + ) + artifact = LoadedArtifact( + chunk=Chunk( + max_stack=1, + protos=[wrapper], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 2), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("neutral/open-tailcall.luac", artifact), + ) + ) + ) + + self.assertEqual( + ( + ( + "root.0@pc3:r1", + "root@pc1:r0", + 0, + ( + "bytecode-only,resolved-call," + "structural-tailcall-producer-proven-open-argument" + ), + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if "structural-tailcall-producer-proven-open-argument" + in flow.provenance + ), + ) + self.assertIn( + ("root.0@pc3", "open-tailcall-argument-tail"), + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + }, + ) + + def test_fixed_arguments_map_only_to_existing_nonvararg_parameters(self) -> None: + callee = Chunk( + num_params=2, + is_vararg=False, + max_stack=2, + instructions=[], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=4, + protos=[callee], + constants=[ + Constant("string", "first"), + Constant("string", "second"), + Constant("string", "discarded"), + ], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 2, 1), + Instruction(Opcode.LOADK, InstructionType.ABx, 3, 2), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 4, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/fixed.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "neutral/fixed.luac", + "root", + "root@pc4", + "neutral/fixed.luac", + "root.0", + "root@pc4:r1", + "root.0:r0", + "argument-to-parameter", + 0, + "bytecode-only,resolved-call,fixed-argument", + ), + ( + "neutral/fixed.luac", + "root", + "root@pc4", + "neutral/fixed.luac", + "root.0", + "root@pc4:r2", + "root.0:r1", + "argument-to-parameter", + 1, + "bytecode-only,resolved-call,fixed-argument", + ), + ), + tuple( + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + ), + ) + self.assertFalse( + any(boundary.site_id == "root@pc4" for boundary in result.boundaries) + ) + + def test_fixed_extra_arguments_flow_only_to_proven_vararg_outputs(self) -> None: + callee = Chunk( + num_params=1, + is_vararg=True, + max_stack=3, + instructions=[ + Instruction(Opcode.VARARG, InstructionType.ABC, 1, 3, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=5, + protos=[callee], + constants=[ + Constant("string", "fixed"), + Constant("string", "first"), + Constant("string", "second"), + Constant("string", "unused"), + ], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 2, 1), + Instruction(Opcode.LOADK, InstructionType.ABx, 3, 2), + Instruction(Opcode.LOADK, InstructionType.ABx, 4, 3), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 5, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/vararg.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "root@pc5:r1", + "root.0:r0", + "argument-to-parameter", + 0, + "bytecode-only,resolved-call,fixed-argument", + ), + ( + "root@pc5:r2", + "root.0@pc0:r1", + "argument-to-vararg", + 1, + "bytecode-only,resolved-call,fixed-vararg", + ), + ( + "root@pc5:r3", + "root.0@pc0:r2", + "argument-to-vararg", + 2, + "bytecode-only,resolved-call,fixed-vararg", + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + ), + ) + self.assertFalse( + any(flow.source_ref == "root@pc5:r4" for flow in result.interprocedural_flows) + ) + + def test_open_call_maps_only_producer_proven_argument_to_parameter(self) -> None: + callee = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[], + ) + producer = Chunk( + num_params=0, + is_vararg=False, + max_stack=1, + instructions=[], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[callee, producer], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 1, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 0, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/open-call.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "neutral/open-call.luac", + "root", + "root@pc3", + "neutral/open-call.luac", + "root.0", + "root@pc3:r1", + "root.0:r0", + "argument-to-parameter", + 0, + "bytecode-only,resolved-call,producer-proven-open-argument", + ), + ), + tuple( + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + ), + ) + self.assertIn( + ("root@pc3", "open-call-argument-tail"), + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + }, + ) + + def test_open_call_maps_proven_argument_to_fixed_vararg_output(self) -> None: + callee = Chunk( + num_params=0, + is_vararg=True, + max_stack=1, + instructions=[ + Instruction(Opcode.VARARG, InstructionType.ABC, 0, 2, 0), + ], + ) + producer = Chunk( + num_params=0, + is_vararg=False, + max_stack=1, + instructions=[], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[callee, producer], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 1, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 0, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/open-vararg.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "root@pc3:r1", + "root.0@pc0:r0", + "argument-to-vararg", + 0, + "bytecode-only,resolved-call,producer-proven-open-vararg", + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + ), + ) + self.assertIn( + ("root@pc3", "open-call-argument-tail"), + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + }, + ) + + def test_fixed_callee_return_flows_to_fixed_caller_result(self) -> None: + callee = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[callee], + constants=[Constant("string", "value")], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 2, 2), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/fixed-return.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "neutral/fixed-return.luac", + "root", + "root@pc2", + "neutral/fixed-return.luac", + "root.0", + "root.0@pc0:r0", + "root@pc2:r0", + "return-to-result", + 0, + "bytecode-only,resolved-call,fixed-return", + ), + ), + tuple( + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertIn( + ("root@pc2:r1", "root.0:r0", "argument-to-parameter"), + { + (flow.source_ref, flow.sink_ref, flow.flow_kind) + for flow in result.interprocedural_flows + }, + ) + + def test_fixed_returns_map_only_to_accepted_caller_results(self) -> None: + callee = Chunk( + num_params=3, + is_vararg=False, + max_stack=3, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 4, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=4, + protos=[callee], + constants=[ + Constant("string", "first"), + Constant("string", "second"), + Constant("string", "third"), + ], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 2, 1), + Instruction(Opcode.LOADK, InstructionType.ABx, 3, 2), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 4, 3), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/multiple-return.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "neutral/multiple-return.luac", + "root", + "root@pc4", + "neutral/multiple-return.luac", + "root.0", + "root.0@pc0:r0", + "root@pc4:r0", + "return-to-result", + 0, + "bytecode-only,resolved-call,fixed-return", + ), + ( + "neutral/multiple-return.luac", + "root", + "root@pc4", + "neutral/multiple-return.luac", + "root.0", + "root.0@pc0:r1", + "root@pc4:r1", + "return-to-result", + 1, + "bytecode-only,resolved-call,fixed-return", + ), + ), + tuple( + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + + def test_multiple_return_sites_preserve_caller_callsite_ownership(self) -> None: + callee = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.TEST, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=4, + protos=[callee], + constants=[ + Constant("string", "first"), + Constant("string", "second"), + ], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 2, 2), + Instruction(Opcode.LOADK, InstructionType.ABx, 3, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 2, 2, 2), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/balanced-return.luac", caller),) + ) + ) + + self.assertEqual( + ( + ("root@pc3", "root.0@pc1:r0", "root@pc3:r0", 0), + ("root@pc3", "root.0@pc2:r0", "root@pc3:r0", 0), + ("root@pc5", "root.0@pc1:r0", "root@pc5:r2", 0), + ("root@pc5", "root.0@pc2:r0", "root@pc5:r2", 0), + ), + tuple( + ( + flow.callsite_id, + flow.source_ref, + flow.sink_ref, + flow.position, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + + def test_cross_module_fixed_return_preserves_module_ownership(self) -> None: + fixture_dir = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call" + ) + controller = Lua51Loader((fixture_dir / "controller.luac").read_bytes()).load() + samplelib = Lua51Loader((fixture_dir / "samplelib.luac").read_bytes()).load() + controller.chunk.protos[0].instructions[10] = Instruction( + Opcode.CALL, + InstructionType.ABC, + 2, + 2, + 2, + ) + samplelib.chunk.protos[0].instructions[4] = Instruction( + Opcode.RETURN, + InstructionType.ABC, + 1, + 2, + 0, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("app/controller.luac", controller), + CorpusArtifact("lib/samplelib.luac", samplelib), + ) + ) + ) + + self.assertEqual( + ( + ( + "app/controller.luac", + "root.0@pc10", + "lib/samplelib.luac", + "root.0@pc3:r1", + "root.0@pc10:r2", + 0, + "bytecode-only,resolved-call,structural-tailcall-result", + ), + ( + "app/controller.luac", + "root.0@pc10", + "lib/samplelib.luac", + "root.0@pc3:r2", + "root.0@pc10:r2", + 0, + "bytecode-only,resolved-call,structural-tailcall-argument", + ), + ( + "app/controller.luac", + "root.0@pc10", + "lib/samplelib.luac", + "root.0@pc4:r1", + "root.0@pc10:r2", + 0, + "bytecode-only,resolved-call,fixed-return", + ), + ), + tuple( + ( + flow.caller_module_path, + flow.callsite_id, + flow.callee_module_path, + flow.source_ref, + flow.sink_ref, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + and flow.callsite_id == "root.0@pc10" + ), + ) + + def test_open_callee_return_maps_only_producer_proven_fixed_result(self) -> None: + callee = Chunk( + num_params=0, + is_vararg=True, + max_stack=1, + instructions=[ + Instruction(Opcode.VARARG, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 0, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=1, + protos=[callee], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 2), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/open-return.luac", caller),) + ) + ) + + self.assertEqual( + ( + ( + "root.0@pc1:r0", + "root@pc1:r0", + "return-to-result", + 0, + "bytecode-only,resolved-call,producer-proven-open-return", + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertTrue( + { + ("root.0@pc0", "open-vararg-tail"), + ("root.0@pc1", "open-return-tail"), + }.issubset( + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + } + ) + ) + + def test_fixed_returns_map_only_proven_open_caller_results(self) -> None: + callee = Chunk( + num_params=2, + is_vararg=False, + max_stack=2, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 3, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=4, + protos=[callee], + constants=[ + Constant("string", "first"), + Constant("string", "second"), + ], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 2, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 3, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("neutral/open-caller-results.luac", caller), + ) + ) + ) + + self.assertEqual( + ( + ( + "root.0@pc0:r0", + "root@pc3:r0", + "return-to-result", + 0, + "bytecode-only,resolved-call,producer-proven-open-result", + ), + ( + "root.0@pc0:r1", + "root@pc3:r1", + "return-to-result", + 1, + "bytecode-only,resolved-call,producer-proven-open-result", + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertFalse( + any( + flow.position >= 2 + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ) + ) + self.assertIn( + ("root@pc3", "open-call-result-tail"), + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + }, + ) + + def test_proven_open_return_maps_only_proven_open_caller_result(self) -> None: + callee = Chunk( + num_params=0, + is_vararg=True, + max_stack=1, + instructions=[ + Instruction(Opcode.VARARG, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 0, 0), + ], + ) + caller = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[callee], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("neutral/open-return-open-result.luac", caller), + ) + ) + ) + + self.assertEqual( + ( + ( + "root.0@pc1:r0", + "root@pc1:r0", + "return-to-result", + 0, + ( + "bytecode-only,resolved-call,producer-proven-open-return," + "producer-proven-open-result" + ), + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertTrue( + { + ("root.0@pc0", "open-vararg-tail"), + ("root.0@pc1", "open-return-tail"), + ("root@pc1", "open-call-result-tail"), + }.issubset( + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + } + ) + ) + + def test_resolved_tailcall_maps_fixed_argument_to_parameter(self) -> None: + callee = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + wrapper = LoadedArtifact( + chunk=Chunk( + num_params=1, + is_vararg=False, + max_stack=3, + protos=[callee], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 1, 2, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 1, 0, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/tailcall-argument.luac", wrapper),) + ) + ) + + self.assertEqual( + ( + ( + "root@pc2:r2", + "root.0:r0", + "argument-to-parameter", + 0, + "bytecode-only,resolved-tailcall,fixed-argument", + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "argument-to-parameter" + ), + ) + self.assertIn( + ("root@pc3", "open-return-tail"), + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + }, + ) + + def test_fixed_return_maps_to_resolved_tailcall_result(self) -> None: + callee = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + wrapper = LoadedArtifact( + chunk=Chunk( + num_params=1, + is_vararg=False, + max_stack=3, + protos=[callee], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 1, 2, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 1, 0, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/tailcall-return.luac", wrapper),) + ) + ) + + self.assertEqual( + ( + ( + "root.0@pc0:r0", + "root@pc2:r1", + "return-to-result", + 0, + "bytecode-only,resolved-tailcall,producer-proven-open-result", + ), + ), + tuple( + ( + flow.source_ref, + flow.sink_ref, + flow.flow_kind, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertIn( + ("root@pc2:r2", "root.0:r0"), + { + (flow.source_ref, flow.sink_ref) + for flow in result.interprocedural_flows + if flow.flow_kind == "argument-to-parameter" + }, + ) + self.assertIn( + ("root@pc3", "open-return-tail"), + { + (boundary.site_id, boundary.boundary_kind) + for boundary in result.boundaries + }, + ) + + def test_resolved_tailcall_result_forwards_to_outer_call_result(self) -> None: + inner = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + wrapper = Chunk( + num_params=1, + is_vararg=False, + max_stack=3, + protos=[inner], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 1, 2, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 1, 0, 0), + ], + ) + outer = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[wrapper], + constants=[Constant("string", "value")], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 2, 2), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/tailcall-forward.luac", outer),) + ) + ) + + self.assertEqual( + ( + ( + "root", + "root@pc2", + "root.0", + "root.0@pc2:r1", + "root@pc2:r0", + 0, + ( + "bytecode-only,resolved-call,resolved-tailcall," + "tailcall-forward" + ), + ), + ( + "root.0", + "root.0@pc2", + "root.0.0", + "root.0.0@pc0:r0", + "root.0@pc2:r1", + 0, + "bytecode-only,resolved-tailcall,producer-proven-open-result", + ), + ), + tuple( + ( + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.position, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertNotIn( + ("root.0.0@pc0:r0", "root@pc2:r0"), + { + (flow.source_ref, flow.sink_ref) + for flow in result.interprocedural_flows + }, + ) + + def test_resolved_tailcall_chain_forwards_each_wrapper_result(self) -> None: + def tail_wrapper(child: Chunk) -> Chunk: + return Chunk( + num_params=1, + is_vararg=False, + max_stack=3, + protos=[child], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 1, 2, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 1, 0, 0), + ], + ) + + inner = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + wrapper_two = tail_wrapper(inner) + wrapper_one = tail_wrapper(wrapper_two) + outer = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[wrapper_one], + constants=[Constant("string", "value")], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 2, 2), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("neutral/tailcall-chain.luac", outer),) + ) + ) + + self.assertEqual( + ( + ( + "root", + "root@pc2", + "root.0", + "root.0@pc2:r1", + "root@pc2:r0", + "bytecode-only,resolved-call,resolved-tailcall,tailcall-forward", + ), + ( + "root.0", + "root.0@pc2", + "root.0.0", + "root.0.0@pc2:r1", + "root.0@pc2:r1", + "bytecode-only,resolved-tailcall,tailcall-chain-forward", + ), + ( + "root.0.0", + "root.0.0@pc2", + "root.0.0.0", + "root.0.0.0@pc0:r0", + "root.0.0@pc2:r1", + "bytecode-only,resolved-tailcall,producer-proven-open-result", + ), + ), + tuple( + ( + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + self.assertTrue( + { + ("root.0.0.0@pc0:r0", "root.0@pc2:r1"), + ("root.0.0.0@pc0:r0", "root@pc2:r0"), + ("root.0.0@pc2:r1", "root@pc2:r0"), + }.isdisjoint( + { + (flow.source_ref, flow.sink_ref) + for flow in result.interprocedural_flows + } + ) + ) + + def test_cross_module_tailcall_preserves_scope_and_rejects_unresolved_chain( + self, + ) -> None: + fixture_dir = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call" + ) + + def load_artifacts() -> tuple[LoadedArtifact, LoadedArtifact]: + return ( + Lua51Loader((fixture_dir / "controller.luac").read_bytes()).load(), + Lua51Loader((fixture_dir / "samplelib.luac").read_bytes()).load(), + ) + + controller, samplelib = load_artifacts() + original = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("app/controller.luac", controller), + CorpusArtifact("lib/samplelib.luac", samplelib), + ) + ) + ) + self.assertFalse( + any( + flow.flow_kind == "return-to-result" + for flow in original.interprocedural_flows + ) + ) + + controller, samplelib = load_artifacts() + inner = Chunk( + num_params=1, + is_vararg=False, + max_stack=1, + instructions=[ + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 2, 0), + ], + ) + samplelib.chunk.protos[0] = Chunk( + num_params=1, + is_vararg=False, + max_stack=3, + protos=[inner], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.MOVE, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.TAILCALL, InstructionType.ABC, 1, 2, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 1, 0, 0), + ], + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("app/controller.luac", controller), + CorpusArtifact("lib/samplelib.luac", samplelib), + ) + ) + ) + + self.assertEqual( + ( + ( + "app/controller.luac", + "root.0", + "root.0@pc10", + "lib/samplelib.luac", + "root.0", + "root.0@pc2:r1", + "root.0@pc10:r2", + "bytecode-only,resolved-tailcall,tailcall-chain-forward", + ), + ( + "app/controller.luac", + "root.1", + "root.1@pc10", + "lib/samplelib.luac", + "root.0", + "root.0@pc2:r1", + "root.1@pc10:r1", + "bytecode-only,resolved-tailcall,tailcall-chain-forward", + ), + ( + "lib/samplelib.luac", + "root.0", + "root.0@pc2", + "lib/samplelib.luac", + "root.0.0", + "root.0.0@pc0:r0", + "root.0@pc2:r1", + "bytecode-only,resolved-tailcall,producer-proven-open-result", + ), + ), + tuple( + ( + flow.caller_module_path, + flow.caller_prototype_id, + flow.callsite_id, + flow.callee_module_path, + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.flow_kind == "return-to-result" + ), + ) + + def test_required_field_target_survives_source_before_require_order(self) -> None: + fixture_dir = ( + Path(__file__).resolve().parents[2] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "app/controller.luac", + Lua51Loader((fixture_dir / "controller.luac").read_bytes()).load(), + ), + CorpusArtifact( + "lib/samplelib.luac", + Lua51Loader((fixture_dir / "samplelib.luac").read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "root.1@pc10:r1", + "samplelib.run", + "module-field-export", + "lib/samplelib.luac", + "root.0", + "bytecode-only,literal-require,module-return-table", + ) + }, + { + ( + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.caller_module_path == "app/controller.luac" + and resolution.callsite_id == "root.1@pc10" + }, + ) + self.assertFalse( + any( + boundary.module_path == "app/controller.luac" + and boundary.site_id == "root.1@pc10" + and boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_missing_required_module_field_retains_unresolved_boundary(self) -> None: + test_dir = Path(__file__).resolve().parent + library = ( + test_dir.parents[1] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call/samplelib.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "app/controller.luac", + Lua51Loader( + (test_dir / "fixtures/module_missing_field_caller.luac").read_bytes() + ).load(), + ), + CorpusArtifact( + "lib/samplelib.luac", + Lua51Loader(library.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual("matched", result.module_resolutions[0].status) + self.assertEqual( + "lib/samplelib.luac", + result.module_resolutions[0].target_module_path, + ) + self.assertFalse( + any( + resolution.caller_module_path == "app/controller.luac" + and resolution.callsite_id == "root@pc5" + for resolution in result.call_resolutions + ) + ) + self.assertEqual( + {("root@pc5", "no-proven-target")}, + { + (boundary.site_id, boundary.reason) + for boundary in result.boundaries + if boundary.module_path == "app/controller.luac" + and boundary.boundary_kind == "unresolved-call-target" + and boundary.site_id == "root@pc5" + }, + ) + + def test_multiple_required_module_field_targets_are_all_resolved(self) -> None: + test_dir = Path(__file__).resolve().parent + controller = ( + test_dir.parents[1] + / "ql/test/library-tests/interprocedural-module-taint" + / "module-return-table-field-call/controller.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "app/controller.luac", + Lua51Loader(controller.read_bytes()).load(), + ), + CorpusArtifact( + "lib/samplelib.luac", + Lua51Loader( + (test_dir / "fixtures/module_multiple_field_exports.luac").read_bytes() + ).load(), + ), + ) + ) + ) + + self.assertEqual( + {"root.0", "root.1"}, + { + export.target_prototype_id + for export in result.module_exports + if export.module_path == "lib/samplelib.luac" + and export.field_name == "run" + }, + ) + self.assertEqual( + { + ( + "root.0", + "root.0@pc10:r2", + "samplelib.run", + "bytecode-only,literal-require,module-global-export,module-call", + ), + ( + "root.1", + "root.0@pc10:r2", + "samplelib.run", + "bytecode-only,literal-require,module-global-export,module-call", + ), + }, + { + ( + resolution.target_prototype_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.caller_module_path == "app/controller.luac" + and resolution.callsite_id == "root.0@pc10" + }, + ) + self.assertEqual( + { + ( + "root.0", + "root.0@pc10:r3", + "root.0:r0", + "bytecode-only,resolved-tailcall,fixed-argument", + ), + ( + "root.1", + "root.0@pc10:r3", + "root.1:r0", + "bytecode-only,resolved-tailcall,fixed-argument", + ), + }, + { + ( + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.caller_module_path == "app/controller.luac" + and flow.callsite_id == "root.0@pc10" + and flow.flow_kind == "argument-to-parameter" + }, + ) + self.assertEqual( + { + ( + "root.0", + "root.0@pc0:r0", + "root.0@pc10:r2", + "bytecode-only,resolved-tailcall,producer-proven-open-result", + ), + ( + "root.1", + "root.1@pc0:r0", + "root.0@pc10:r2", + "bytecode-only,resolved-tailcall,producer-proven-open-result", + ), + }, + { + ( + flow.callee_prototype_id, + flow.source_ref, + flow.sink_ref, + flow.provenance, + ) + for flow in result.interprocedural_flows + if flow.caller_module_path == "app/controller.luac" + and flow.callsite_id == "root.0@pc10" + and flow.flow_kind == "return-to-result" + }, + ) + self.assertFalse( + any( + boundary.module_path == "app/controller.luac" + and boundary.site_id == "root.0@pc10" + and boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_required_nested_static_module_field_resolves_exact_closure(self) -> None: + test_dir = Path(__file__).resolve().parent + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "app/controller.luac", + Lua51Loader( + (test_dir / "fixtures/module_nested_field_caller.luac").read_bytes() + ).load(), + ), + CorpusArtifact( + "lib/sample.luac", + Lua51Loader( + (test_dir / "fixtures/module_nested_field_provider.luac").read_bytes() + ).load(), + ), + ) + ) + ) + + positive = [ + resolution + for resolution in result.call_resolutions + if resolution.caller_module_path == "app/controller.luac" + and resolution.resolved_name == "sample.handlers.run" + ] + self.assertEqual(1, len(positive)) + self.assertEqual("module-field-export", positive[0].resolution_kind) + self.assertEqual("lib/sample.luac", positive[0].target_module_path) + self.assertEqual("root.0", positive[0].target_prototype_id) + + self.assertEqual( + {"argument-to-parameter", "return-to-result"}, + { + flow.flow_kind + for flow in result.interprocedural_flows + if flow.caller_module_path == "app/controller.luac" + and flow.callsite_id == positive[0].callsite_id + and flow.callee_module_path == "lib/sample.luac" + and flow.callee_prototype_id == "root.0" + }, + ) + + unresolved_names = { + resolution.resolved_name + for resolution in result.call_resolutions + if resolution.caller_module_path == "app/controller.luac" + and not resolution.target_module_path + and not resolution.target_prototype_id + } + self.assertIn("sample.handlers.missing", unresolved_names) + self.assertEqual( + ["no-proven-target"], + [ + boundary.reason + for boundary in result.boundaries + if boundary.module_path == "app/controller.luac" + and boundary.boundary_kind == "unresolved-call-target" + ], + ) + self.assertFalse( + any( + resolution.target_module_path == "lib/sample.luac" + and resolution.target_prototype_id == "root.0" + and resolution.resolved_name != "sample.handlers.run" + for resolution in result.call_resolutions + ) + ) + + def test_same_module_global_call_resolves_exported_closure(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_same_module_export_call.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "samplelib.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "samplelib.luac", + "root.1", + "root.1@pc2", + "root.1@pc2:r1", + "helper", + "same-module-field-export", + "samplelib.luac", + "root.0", + "bytecode-only,same-module,module-global-export,module-call", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.callsite_id == "root.1@pc2" + }, + ) + + def test_same_module_global_table_call_resolves_exported_closure(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_same_module_global_table_call.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "sample.luac", + "root.1", + "root.1@pc3", + "root.1@pc3:r1", + "handlers.run", + "same-module-field-export", + "sample.luac", + "root.0", + ( + "bytecode-only,same-module," + "module-global-table-field-export,module-call" + ), + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.callsite_id == "root.1@pc3" + }, + ) + + def test_multiple_same_module_global_targets_remain_name_only(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_multiple_same_module_exports.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + {"root.0", "root.1"}, + { + export.target_prototype_id + for export in result.module_exports + if export.export_kind == "module-global" + and export.field_name == "helper" + }, + ) + self.assertEqual( + { + ( + "helper", + "derived-reaching-definition-call-target", + "", + "", + ) + }, + { + ( + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + if resolution.caller_module_path == "sample.luac" + and resolution.callsite_id == "root.2@pc2" + }, + ) + + def test_multiple_same_module_table_field_targets_remain_name_only( + self, + ) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/module_multiple_same_module_global_table_exports.luac" + ) + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "sample.luac", + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + {"root.0", "root.1"}, + { + export.target_prototype_id + for export in result.module_exports + if export.export_kind == "module-global-table-field" + and export.field_name == "handlers.run" + }, + ) + self.assertEqual( + { + ( + "handlers.run", + "derived-reaching-definition-call-target", + "", + "", + ) + }, + { + ( + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + if resolution.caller_module_path == "sample.luac" + and resolution.callsite_id == "root.2@pc3" + }, + ) + + def test_closure_call_target_uses_only_precise_table_field_flow(self) -> None: + def loaded(read_key_index: int) -> LoadedArtifact: + return LoadedArtifact( + chunk=Chunk( + max_stack=3, + constants=[ + Constant("string", "handler"), + Constant("string", "missing"), + ], + protos=[Chunk(max_stack=1, instructions=[])], + instructions=[ + Instruction(Opcode.NEWTABLE, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.SETTABLE, InstructionType.ABC, 0, 256, 1), + Instruction( + Opcode.GETTABLE, + InstructionType.ABC, + 2, + 0, + 256 + read_key_index, + ), + Instruction(Opcode.CALL, InstructionType.ABC, 2, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("positive/table.luac", loaded(0)), + CorpusArtifact("negative/missing-field.luac", loaded(1)), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/table.luac", + "root", + "root@pc4", + "root@pc4:r2", + "", + "closure-table-field", + "positive/table.luac", + "root.0", + "bytecode-only,closure-table-field-target", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_captured_local_table_field_call_resolves_unique_closure(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/captured_local_table_field_closure.luac" + ) + module_path = "neutral/captured-local-table-field-closure.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + "root.1", + "root.1@pc3", + module_path, + "root.0", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_module_path, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + if resolution.callsite_id == "root.1@pc3" + }, + ) + self.assertFalse( + any( + boundary.prototype_id == "root.1" + and boundary.site_id == "root.1@pc3" + and boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_captured_local_table_field_overwrite_drops_old_closure(self) -> None: + fixture = ( + Path(__file__).resolve().parent + / "fixtures/captured_local_table_field_overwrite.luac" + ) + module_path = "neutral/captured-local-table-field-overwrite.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertNotIn( + ("root.1", "root.1@pc3"), + { + (resolution.caller_prototype_id, resolution.callsite_id) + for resolution in result.call_resolutions + }, + ) + self.assertIn( + ("root.1", "root.1@pc3", "unresolved-call-target"), + { + ( + boundary.prototype_id, + boundary.site_id, + boundary.boundary_kind, + ) + for boundary in result.boundaries + }, + ) + + def test_closure_call_target_uses_only_precise_global_flow(self) -> None: + def loaded(read_name_index: int) -> LoadedArtifact: + return LoadedArtifact( + chunk=Chunk( + max_stack=2, + constants=[ + Constant("string", "handler"), + Constant("string", "missing"), + ], + protos=[Chunk(max_stack=1, instructions=[])], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.SETGLOBAL, InstructionType.ABx, 0, 0), + Instruction( + Opcode.GETGLOBAL, + InstructionType.ABx, + 1, + read_name_index, + ), + Instruction(Opcode.CALL, InstructionType.ABC, 1, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("positive/global.luac", loaded(0)), + CorpusArtifact("negative/missing-global.luac", loaded(1)), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/global.luac", + "root", + "root@pc3", + "root@pc3:r1", + "handler", + "closure-global", + "positive/global.luac", + "root.0", + "bytecode-only,closure-global-target", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.target_prototype_id + }, + ) + + def test_resolved_closure_target_retains_reaching_name(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/experimental/query-tests/rules-sanitizer-report" + / "sanitizer-on-path/input.luac" + ) + module_path = "neutral/named-closure-sink.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ( + module_path, + "root@pc24", + "os.execute", + "closure-global", + module_path, + "root.0", + ), + { + ( + resolution.caller_module_path, + resolution.callsite_id, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + }, + ) + + def test_closure_call_target_uses_upvalue_capture_and_first_mutation(self) -> None: + caller = Chunk( + num_upvalues=1, + max_stack=3, + protos=[Chunk(max_stack=1, instructions=[])], + instructions=[ + Instruction(Opcode.GETUPVAL, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 0), + Instruction(Opcode.SETUPVAL, InstructionType.ABC, 1, 0, 0), + Instruction(Opcode.GETUPVAL, InstructionType.ABC, 2, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 2, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ) + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + protos=[Chunk(max_stack=1, instructions=[]), caller], + instructions=[ + Instruction(Opcode.CLOSURE, InstructionType.ABx, 0, 0), + Instruction(Opcode.CLOSURE, InstructionType.ABx, 1, 1), + Instruction(Opcode.MOVE, InstructionType.ABC, 0, 0, 0), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("positive/upvalue.luac", loaded),) + ) + ) + + self.assertEqual( + { + ( + "root.1@pc1", + "root.1@pc1:r0", + "closure-upvalue", + "root.0", + "bytecode-only,closure-upvalue-target", + ), + ( + "root.1@pc5", + "root.1@pc5:r2", + "closure-upvalue", + "root.1.0", + "bytecode-only,closure-upvalue-target", + ), + }, + { + ( + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolution_kind, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_adjacent_global_call_uses_only_exact_string_name(self) -> None: + def loaded(constants: list[Constant]) -> LoadedArtifact: + return LoadedArtifact( + chunk=Chunk( + max_stack=1, + constants=constants, + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "positive/global-name.luac", + loaded([Constant("string", "handler")]), + ), + CorpusArtifact("negative/missing-name.luac", loaded([])), + CorpusArtifact( + "negative/non-string-name.luac", + loaded([Constant("number", 1.0)]), + ), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/global-name.luac", + "root", + "root@pc1", + "root@pc1:r0", + "handler", + "derived-adjacent-global-name", + "", + "", + "bytecode-only,derived-call-target", + ) + }, + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_module_path, + resolution.target_prototype_id, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_reaching_global_member_call_requires_exact_member_key(self) -> None: + def loaded(member_operand: int) -> LoadedArtifact: + return LoadedArtifact( + chunk=Chunk( + max_stack=3, + constants=[ + Constant("string", "os"), + Constant("string", "execute"), + Constant("string", "argument"), + ], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 1, 0), + Instruction( + Opcode.GETTABLE, + InstructionType.ABC, + 1, + 1, + member_operand, + ), + Instruction(Opcode.LOADK, InstructionType.ABx, 2, 2), + Instruction(Opcode.CALL, InstructionType.ABC, 1, 2, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "positive/reaching-member.luac", + loaded(256 + 1), + ), + CorpusArtifact("negative/dynamic-member.luac", loaded(1)), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/reaching-member.luac", + "root@pc3", + "root@pc3:r1", + "os.execute", + "derived-reaching-definition-call-target", + "bytecode-only,derived-call-target,reaching-definition", + ) + }, + { + ( + resolution.caller_module_path, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_reaching_global_member_chain_resolves_exact_name(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/experimental/query-tests/rules-sanitizer-report" + / "formvalue-os-execute-chain/input.luac" + ) + module_path = "neutral/source-sink-chain.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ( + module_path, + "root", + "root@pc16", + "root@pc16:r0", + "luci.http.formvalue", + "derived-reaching-definition-call-target", + "bytecode-only,derived-call-target,reaching-definition", + ), + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + self.assertFalse( + any( + boundary.site_id == "root@pc16" and + boundary.boundary_kind == "unresolved-call-target" + for boundary in result.boundaries + ) + ) + + def test_local_closure_call_retains_exact_debug_local_name(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/experimental/query-tests/rules-sanitizer-report" + / "sanitizer-on-path/input.luac" + ) + module_path = "neutral/local-closure-call.luac" + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + module_path, + Lua51Loader(fixture.read_bytes()).load(), + ), + ) + ) + ) + + self.assertIn( + ( + module_path, + "root", + "root@pc20", + "root@pc20:r2", + "tonumber", + "closure-move", + "bytecode-only,closure-move-target", + ), + { + ( + resolution.caller_module_path, + resolution.caller_prototype_id, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_local_closure_call_without_debug_local_remains_unnamed(self) -> None: + fixture = ( + Path(__file__).resolve().parents[2] + / "ql/test/experimental/query-tests/rules-sanitizer-report" + / "sanitizer-on-path/input.luac" + ) + loaded = Lua51Loader(fixture.read_bytes()).load() + assert loaded.chunk is not None + loaded.chunk.locals = [] + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact("neutral/stripped-local.luac", loaded), + ) + ) + ) + + self.assertIn( + ( + "root@pc20", + "", + "closure-move", + "root.1", + ), + { + ( + resolution.callsite_id, + resolution.resolved_name, + resolution.resolution_kind, + resolution.target_prototype_id, + ) + for resolution in result.call_resolutions + }, + ) + + def test_self_parameter_member_call_requires_exact_member_key(self) -> None: + def loaded(member_operand: int) -> LoadedArtifact: + return LoadedArtifact( + chunk=Chunk( + num_params=1, + max_stack=2, + constants=[Constant("string", "match")], + instructions=[ + Instruction(Opcode.JMP, InstructionType.AsBx, 0, 0), + Instruction( + Opcode.SELF, + InstructionType.ABC, + 0, + 0, + member_operand, + ), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 2, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "positive/self-parameter.luac", + loaded(256), + ), + CorpusArtifact("negative/dynamic-self.luac", loaded(0)), + ) + ) + ) + + self.assertEqual( + { + ( + "positive/self-parameter.luac", + "root@pc2", + "root@pc2:r0", + "Param_0.match", + "derived-reaching-definition-call-target", + "bytecode-only,derived-call-target,reaching-definition", + ) + }, + { + ( + resolution.caller_module_path, + resolution.callsite_id, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.provenance, + ) + for resolution in result.call_resolutions + }, + ) + + def test_self_member_call_reuses_every_named_call_result(self) -> None: + def regular_call( + result_operand: int, + base_register: int, + member_operand: int = 256 + 1, + ) -> LoadedArtifact: + return LoadedArtifact( + chunk=Chunk( + max_stack=5, + constants=[ + Constant("string", "factory"), + Constant("string", "run"), + ], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction( + Opcode.CALL, + InstructionType.ABC, + 0, + 1, + result_operand, + ), + Instruction( + Opcode.SELF, + InstructionType.ABC, + 3, + base_register, + member_operand, + ), + Instruction(Opcode.CALL, InstructionType.ABC, 3, 2, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + require_multiple_results = LoadedArtifact( + chunk=Chunk( + max_stack=5, + constants=[ + Constant("string", "require"), + Constant("string", "sample.module"), + Constant("string", "run"), + ], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.LOADK, InstructionType.ABx, 1, 1), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 2, 3), + Instruction( + Opcode.SELF, + InstructionType.ABC, + 3, + 1, + 256 + 2, + ), + Instruction(Opcode.CALL, InstructionType.ABC, 3, 2, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + iterator_derived_result = LoadedArtifact( + chunk=Chunk( + max_stack=7, + constants=[ + Constant("string", "factory"), + Constant("string", "run"), + ], + instructions=[ + Instruction(Opcode.GETGLOBAL, InstructionType.ABx, 0, 0), + Instruction(Opcode.CALL, InstructionType.ABC, 0, 1, 4), + Instruction(Opcode.TFORLOOP, InstructionType.ABC, 0, 0, 2), + Instruction( + Opcode.SELF, + InstructionType.ABC, + 5, + 4, + 256 + 1, + ), + Instruction(Opcode.CALL, InstructionType.ABC, 5, 2, 1), + Instruction(Opcode.RETURN, InstructionType.ABC, 0, 1, 0), + ], + ), + profile={}, + profile_id="test-profile", + accepted=True, + ) + + result = analyze_corpus( + AcceptedCorpus( + artifacts=( + CorpusArtifact( + "positive/single-call-result.luac", + regular_call(2, 0), + ), + CorpusArtifact( + "positive/fixed-multiple-call-results.luac", + regular_call(4, 1), + ), + CorpusArtifact( + "positive/iterator-derived-call-result.luac", + iterator_derived_result, + ), + CorpusArtifact( + "negative/unproven-open-call-results.luac", + regular_call(0, 2), + ), + CorpusArtifact( + "negative/zero-call-results.luac", + regular_call(1, 1), + ), + CorpusArtifact( + "negative/require-multiple-results.luac", + require_multiple_results, + ), + CorpusArtifact( + "negative/dynamic-call-result-member.luac", + regular_call(4, 1, 2), + ), + ) + ) + ) + + self.assertEqual( + { + ( + module_path, + target_value_ref, + "factory.run", + "derived-reaching-definition-call-target", + "bytecode-only,derived-call-target,reaching-definition", + ) + for module_path, target_value_ref in { + ("positive/single-call-result.luac", "root@pc3:r3"), + ( + "positive/fixed-multiple-call-results.luac", + "root@pc3:r3", + ), + ( + "positive/iterator-derived-call-result.luac", + "root@pc4:r5", + ), + } + }, + { + ( + resolution.caller_module_path, + resolution.target_value_ref, + resolution.resolved_name, + resolution.resolution_kind, + resolution.provenance, + ) + for resolution in result.call_resolutions + if resolution.callsite_id in {"root@pc3", "root@pc4"} + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/lua/tools/tests/test_extractor_trap.py b/lua/tools/tests/test_extractor_trap.py new file mode 100644 index 000000000000..05f9cf110859 --- /dev/null +++ b/lua/tools/tests/test_extractor_trap.py @@ -0,0 +1,104 @@ +from pathlib import Path +import sys +from tempfile import TemporaryDirectory +import unittest + + +TOOLS_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(TOOLS_ROOT)) + +from corpus_analyzer import ( # noqa: E402 + AcceptedCorpus, + CorpusArtifact, + analyze_corpus, +) +from index_lua_files import emit_loaded_bytecode # noqa: E402 +from lua_bytecode import ( # noqa: E402 + Chunk, + Constant, + Instruction, + InstructionType, + LoadedArtifact, + Opcode, +) +from trap_writer import TrapWriter # noqa: E402 + + +class ExtractorTrapTests(unittest.TestCase): + def test_repeated_overwrites_use_analyzer_flow_without_legacy_kill_rows(self) -> None: + instructions: list[Instruction] = [] + for _ in range(32): + instructions.append( + Instruction(Opcode.LOADK, InstructionType.ABx, a=0, b=0) + ) + instructions.append( + Instruction(Opcode.MOVE, InstructionType.ABC, a=1, b=0, c=0) + ) + instructions.append( + Instruction(Opcode.RETURN, InstructionType.ABC, a=1, b=2, c=0) + ) + loaded = LoadedArtifact( + chunk=Chunk( + max_stack=2, + instructions=instructions, + constants=[Constant("string", "value")], + ), + profile={ + "version": 0x51, + "format": 0, + "little_endian": 1, + "int_size": 4, + "size_t_size": 8, + "instruction_size": 4, + "lua_number_size": 8, + "integral_flag": 0, + }, + profile_id="lua51-test", + accepted=True, + ) + analysis = analyze_corpus( + AcceptedCorpus( + artifacts=(CorpusArtifact("scale/input.luac", loaded),) + ) + ) + local_edges = { + (flow.source_ref, flow.sink_ref) + for flow in analysis.value_flows + if flow.module_path == "scale/input.luac" + } + self.assertIn(("root@pc62:r0", "root@pc63:r0"), local_edges) + self.assertNotIn(("root@pc60:r0", "root@pc63:r0"), local_edges) + + with TemporaryDirectory() as directory: + temporary_root = Path(directory) + bytecode_path = temporary_root / "input.luac" + bytecode_path.write_bytes(b"public extractor test artifact") + trap = TrapWriter() + emit_loaded_bytecode( + trap, + bytecode_path, + temporary_root / "source-archive", + loaded, + analysis.artifact_identities[0], + analysis.value_flows, + analysis.control_flow_edges, + analysis.dominator_tree_intervals, + analysis.boundaries, + analysis.call_resolutions, + analysis.literal_requires, + analysis.module_resolutions, + analysis.module_exports, + analysis.interprocedural_flows, + analysis.table_field_flows, + analysis.global_flows, + analysis.upvalue_flows, + ) + + legacy_kill_rows = [ + line for line in trap.lines if line.startswith("lua_kill_overwrites(") + ] + self.assertEqual(0, len(legacy_kill_rows)) + + +if __name__ == "__main__": + unittest.main() diff --git a/lua/tools/tests/test_qhelp_examples.py b/lua/tools/tests/test_qhelp_examples.py new file mode 100644 index 000000000000..979891c7f5af --- /dev/null +++ b/lua/tools/tests/test_qhelp_examples.py @@ -0,0 +1,30 @@ +from pathlib import Path +import unittest + + +LUA_ROOT = Path(__file__).resolve().parents[2] +TEST_ROOT = LUA_ROOT / "ql/test/experimental/query-tests/qhelp-examples" + + +class QhelpExampleContractTests(unittest.TestCase): + def test_query_inputs_are_the_documented_examples(self) -> None: + pairs = ( + ( + LUA_ROOT + / "ql/src/experimental/Security/CWE-078/examples/CommandInjection.lua", + TEST_ROOT / "CommandInjection.lua", + ), + ( + LUA_ROOT + / "ql/src/experimental/Diagnostics/examples/SanitizedCommandFlow.lua", + TEST_ROOT / "SanitizedCommandFlow.lua", + ), + ) + + for documented, tested in pairs: + with self.subTest(example=documented.name): + self.assertEqual(documented.read_bytes(), tested.read_bytes()) + + +if __name__ == "__main__": + unittest.main() diff --git a/lua/tools/tests/test_source_archive.py b/lua/tools/tests/test_source_archive.py new file mode 100644 index 000000000000..e40bfe2206c0 --- /dev/null +++ b/lua/tools/tests/test_source_archive.py @@ -0,0 +1,55 @@ +from pathlib import Path +import shutil +import subprocess +import sys +from tempfile import TemporaryDirectory +import unittest + + +TOOLS_ROOT = Path(__file__).resolve().parents[1] +LUA_ROOT = TOOLS_ROOT.parent + + +class SourceArchiveTests(unittest.TestCase): + def test_extractor_cli_uses_codeql_source_archive_namespace(self) -> None: + fixture = ( + LUA_ROOT + / "ql/test/library-tests/bytecode-model/bc-constants-call/input.luac" + ) + + with TemporaryDirectory() as directory: + temporary_root = Path(directory).resolve() + input_root = temporary_root / "input" + source_archive = temporary_root / "source-archive" + trap_output = temporary_root / "trap" + lua_source = input_root / "app/source.lua" + lua_bytecode = input_root / "lib/input.luac" + lua_source.parent.mkdir(parents=True) + lua_bytecode.parent.mkdir(parents=True) + lua_source.write_text("return 'source archive test'\n", encoding="utf-8") + shutil.copyfile(fixture, lua_bytecode) + + subprocess.run( + [ + sys.executable, + str(TOOLS_ROOT / "index_lua_files.py"), + "--source-root", + str(input_root), + "--source-archive-dir", + str(source_archive), + "--output-dir", + str(trap_output), + ], + cwd=input_root, + check=True, + ) + + for source in (lua_source, lua_bytecode): + archive_path = str(source.resolve()).replace(":", "_").lstrip("/\\") + archived = source_archive / archive_path + self.assertTrue(archived.is_file(), archived) + self.assertEqual(source.read_bytes(), archived.read_bytes()) + + +if __name__ == "__main__": + unittest.main() diff --git a/lua/tools/trap_writer.py b/lua/tools/trap_writer.py new file mode 100644 index 000000000000..254b4e7beecf --- /dev/null +++ b/lua/tools/trap_writer.py @@ -0,0 +1,48 @@ +"""Small TRAP escaping and writing interface for Lua extractor facts.""" + +from __future__ import annotations + +from pathlib import Path +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class TrapLabel: + value: str + + def __str__(self) -> str: + return self.value + + +class TrapWriter: + def __init__(self): + self.lines: list[str] = [] + self.next_id = 1000 + + def label(self, text: str | None = None) -> TrapLabel: + self.next_id += 1 + label = f"#{self.next_id}" + if text is None: + self.lines.append(f"{label}=*") + else: + self.lines.append(f"{label}=@{self.q(text)}") + return TrapLabel(label) + + def tuple(self, relation: str, *args: Any) -> None: + self.lines.append(f"{relation}({','.join(self.arg(a) for a in args)})") + + def arg(self, value: Any) -> str: + if isinstance(value, int): + return str(value) + if isinstance(value, TrapLabel): + return value.value + return self.q(str(value)) + + def q(self, value: str) -> str: + escaped = value.replace('"', '""').replace("\n", "\\n") + return f'"{escaped}"' + + def write(self, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(self.lines) + "\n", encoding="utf-8") diff --git a/misc/scripts/create-change-note.py b/misc/scripts/create-change-note.py index acaf0794c867..935e85e3ec92 100755 --- a/misc/scripts/create-change-note.py +++ b/misc/scripts/create-change-note.py @@ -26,6 +26,7 @@ "go", "java", "javascript", + "lua", "python", "ruby", "rust", diff --git a/misc/scripts/generate-code-scanning-query-list.py b/misc/scripts/generate-code-scanning-query-list.py index 24ed1d5de004..1b2d6e9de229 100755 --- a/misc/scripts/generate-code-scanning-query-list.py +++ b/misc/scripts/generate-code-scanning-query-list.py @@ -30,7 +30,7 @@ assert hasattr(arguments, "ignore_missing_query_packs") # Define which languages and query packs to consider -languages = [ "actions", "cpp", "csharp", "go", "java", "javascript", "python", "ruby", "rust", "swift" ] +languages = [ "actions", "cpp", "csharp", "go", "java", "javascript", "lua", "python", "ruby", "rust", "swift" ] packs = [ "code-scanning", "security-and-quality", "security-extended", "security-experimental", "code-quality", "code-quality-extended"] class CodeQL: @@ -183,9 +183,9 @@ def subprocess_run(cmd): else: sys.exit("You can use '--ignore-missing-query-packs' to ignore this error") - # Exception for the code-quality suites, which might be empty, but must be resolvable. - if pack in ['code-quality', 'code-quality-extended'] and queries_subp == '': - print(f'Warning: skipping empty suite {pack}', file=sys.stderr) + # A suite may resolve successfully without selecting any queries. + if queries_subp == '': + print(f'Warning: skipping empty suite {lang}-{pack}', file=sys.stderr) continue # Investigate metadata for every query by using 'codeql resolve metadata'