From 52a5c2f5438a8f4b5921d04c5b9475173a4cf7bd Mon Sep 17 00:00:00 2001 From: prankster009 Date: Wed, 22 Jul 2026 02:50:57 -0400 Subject: [PATCH] feat(lua): add Lua 5.1 language analysis Add a self-contained extractor, bytecode schema, QL libraries, query packs, security queries, documentation, and tests for Lua 5.1 analysis. --- .github/labeler.yml | 3 + .github/workflows/validate-change-notes.yml | 2 +- lua/README.md | 206 + lua/SCHEMA.md | 74 + lua/codeql-extractor.yml | 22 + lua/lua.dbscheme | 315 + lua/lua.dbscheme.stats | 109 + .../2026-07-21-lua-bytecode-analysis.md | 5 + lua/ql/lib/codeql/lua/Bytecode.qll | 138 + .../codeql/lua/InterproceduralModuleTaint.qll | 319 + .../codeql/lua/IntraproceduralSemantics.qll | 486 ++ .../lib/codeql/lua/RulesSanitizerReport.qll | 769 +++ lua/ql/lib/codeql/lua/SourceFile.qll | 16 + lua/ql/lib/lua.dbscheme | 315 + lua/ql/lib/lua.dbscheme.stats | 109 + lua/ql/lib/lua.qll | 9 + lua/ql/lib/qlpack.yml | 9 + lua/ql/src/README.md | 93 + .../2026-07-21-lua-bytecode-queries.md | 6 + .../lua-code-quality-extended.qls | 4 + lua/ql/src/codeql-suites/lua-code-quality.qls | 4 + .../src/codeql-suites/lua-code-scanning.qls | 4 + .../lua-security-and-quality.qls | 4 + .../lua-security-experimental.qls | 4 + .../codeql-suites/lua-security-extended.qls | 4 + .../Diagnostics/LuaBytecodeFacts.ql | 281 + .../Diagnostics/SanitizedCommandFlow.qhelp | 38 + .../Diagnostics/SanitizedCommandFlow.ql | 61 + .../examples/SanitizedCommandFlow.lua | 4 + lua/ql/src/experimental/README.md | 5 + .../Security/CWE-078/CommandInjection.qhelp | 38 + .../Security/CWE-078/CommandInjection.ql | 44 + .../CWE-078/examples/CommandInjection.lua | 15 + lua/ql/src/qlpack.yml | 12 + .../qhelp-examples/CommandInjection.expected | 48 + .../qhelp-examples/CommandInjection.lua | 15 + .../qhelp-examples/CommandInjection.luac | Bin 0 -> 366 bytes .../qhelp-examples/CommandInjection.qlref | 1 + .../query-tests/qhelp-examples/README.md | 16 + .../SanitizedCommandFlow.expected | 12 + .../qhelp-examples/SanitizedCommandFlow.lua | 4 + .../qhelp-examples/SanitizedCommandFlow.luac | Bin 0 -> 246 bytes .../qhelp-examples/SanitizedCommandFlow.qlref | 1 + .../CommandInjection.expected | 526 ++ .../CommandInjection.qlref | 1 + .../LuaBytecodeSanitizedPath.expected | 61 + .../LuaBytecodeSanitizedPath.qlref | 1 + .../RulesSanitizerReport.expected | 96 + .../RulesSanitizerReport.ql | 115 + .../RulesSanitizerReportNegatives.expected | 1 + .../RulesSanitizerReportNegatives.ql | 116 + .../rules-sanitizer-report/SAMPLE-MANIFEST.md | 31 + .../TableFieldSanitizerOverwrite.expected | 4 + .../TableFieldSanitizerOverwrite.ql | 37 + .../bc-branch-negative/input.luac | Bin 0 -> 504 bytes .../bc-kill-overwrite/input.luac | Bin 0 -> 417 bytes .../bc-taint-minimal-path/input.luac | Bin 0 -> 398 bytes .../callsite-balanced-report/input.luac | Bin 0 -> 216 bytes .../callsite-balanced-report/source.lua | 12 + .../input.luac | Bin 0 -> 339 bytes .../source.lua.txt | 3 + .../cross-module-sanitizer/controller.lua | 15 + .../cross-module-sanitizer/controller.luac | Bin 0 -> 412 bytes .../cross-module-sanitizer/sanitizer.lua | 8 + .../cross-module-sanitizer/sanitizer.luac | Bin 0 -> 175 bytes .../controller.lua.txt | 14 + .../cross-module-webcmd-popen/controller.luac | Bin 0 -> 677 bytes .../cross-module-webcmd-popen/mtkwifi.lua.txt | 13 + .../cross-module-webcmd-popen/mtkwifi.luac | Bin 0 -> 444 bytes .../formvalue-os-execute-chain/input.luac | Bin 0 -> 527 bytes .../formvalue-os-execute-chain/source.lua.txt | 14 + .../input.luac | Bin 0 -> 567 bytes .../source.lua.txt | 15 + .../controller.lua.txt | 15 + .../controller.luac | Bin 0 -> 686 bytes .../shellsafe.lua.txt | 7 + .../shellsafe.luac | Bin 0 -> 235 bytes .../sanitizer-on-path/input.luac | Bin 0 -> 687 bytes .../sanitizer-on-path/source.lua.txt | 19 + .../input.luac | Bin 0 -> 706 bytes .../source.lua.txt | 20 + .../input.luac | Bin 0 -> 766 bytes .../source.lua.txt | 23 + .../submit-dpp-uri-execute/input.luac | Bin 0 -> 710 bytes .../submit-dpp-uri-execute/source.lua.txt | 18 + .../dynamic-key.lua | 7 + .../dynamic-key.luac | Bin 0 -> 343 bytes .../optional-branch.lua | 7 + .../optional-branch.luac | Bin 0 -> 314 bytes .../same-field.lua | 9 + .../same-field.luac | Bin 0 -> 369 bytes .../unrelated-field.lua | 9 + .../unrelated-field.luac | Bin 0 -> 389 bytes lua/ql/test/library-tests/README.md | 41 + .../BytecodeDiagnostics.expected | 5 + .../bytecode-model/BytecodeDiagnostics.ql | 18 + .../bytecode-model/BytecodeModel.expected | 6 + .../bytecode-model/BytecodeModel.ql | 66 + .../bc-constants-call/input.luac | Bin 0 -> 348 bytes .../malformed-constant.luac | Bin 0 -> 398 bytes .../not-lua-bytecode.luac | 1 + .../bc-malformed-diagnostic/truncated.luac | Bin 0 -> 8 bytes .../unsupported-profile.luac | Bin 0 -> 398 bytes .../unsupported-version.luac | Bin 0 -> 398 bytes .../bc-prototype-params/input.luac | Bin 0 -> 340 bytes .../bc-stripped-metadata/input.luac | Bin 0 -> 154 bytes .../.gitattributes | 2 + .../CorpusAcceptance.expected | 19 + .../CorpusAcceptance.ql | 67 + .../SAMPLE-MANIFEST.md | 20 + .../complex-bytecode-integration/SHA256SUMS | 92 + .../lib/wifi/mtwifi.lua | 386 ++ .../lib/wifi/mtwifi.luac | Bin 0 -> 11598 bytes .../lib/wifi/quick_setting.lua | 1033 +++ .../lib/wifi/quick_setting.luac | Bin 0 -> 28098 bytes .../lib/wifi/wifi_services.lua | 189 + .../lib/wifi/wifi_services.luac | Bin 0 -> 5752 bytes .../lib/wifi/wifi_services_sh.lua | 91 + .../lib/wifi/wifi_services_sh.luac | Bin 0 -> 2192 bytes .../usr/lib/lua/l1dat_parser.lua | 349 + .../usr/lib/lua/l1dat_parser.luac | Bin 0 -> 7015 bytes .../usr/lib/lua/ltn12.lua | 319 + .../usr/lib/lua/ltn12.luac | Bin 0 -> 5825 bytes .../usr/lib/lua/luci/cacheloader.lua | 12 + .../usr/lib/lua/luci/cacheloader.luac | Bin 0 -> 300 bytes .../usr/lib/lua/luci/ccache.lua | 76 + .../usr/lib/lua/luci/ccache.luac | Bin 0 -> 1751 bytes .../usr/lib/lua/luci/config.lua | 18 + .../usr/lib/lua/luci/config.luac | Bin 0 -> 592 bytes .../lib/lua/luci/controller/admin/index.lua | 196 + .../lib/lua/luci/controller/admin/index.luac | Bin 0 -> 5080 bytes .../usr/lib/lua/luci/controller/admin/uci.lua | 70 + .../lib/lua/luci/controller/admin/uci.luac | Bin 0 -> 1865 bytes .../usr/lib/lua/luci/controller/hwnat.lua | 54 + .../usr/lib/lua/luci/controller/hwnat.luac | Bin 0 -> 1600 bytes .../usr/lib/lua/luci/controller/ipsec.lua | 72 + .../usr/lib/lua/luci/controller/ipsec.luac | Bin 0 -> 3085 bytes .../usr/lib/lua/luci/controller/mtkwifi.lua | 2738 ++++++++ .../usr/lib/lua/luci/controller/mtkwifi.luac | Bin 0 -> 93192 bytes .../usr/lib/lua/luci/debug.lua | 37 + .../usr/lib/lua/luci/debug.luac | Bin 0 -> 1186 bytes .../usr/lib/lua/luci/dispatcher.lua | 1532 +++++ .../usr/lib/lua/luci/dispatcher.luac | Bin 0 -> 32883 bytes .../usr/lib/lua/luci/http.lua | 554 ++ .../usr/lib/lua/luci/http.luac | Bin 0 -> 12387 bytes .../usr/lib/lua/luci/i18n.lua | 55 + .../usr/lib/lua/luci/i18n.luac | Bin 0 -> 1476 bytes .../usr/lib/lua/luci/ltn12.lua | 316 + .../usr/lib/lua/luci/ltn12.luac | Bin 0 -> 5391 bytes .../usr/lib/lua/luci/model/cbi/hwnat.lua | 12 + .../usr/lib/lua/luci/model/cbi/hwnat.luac | Bin 0 -> 637 bytes .../usr/lib/lua/luci/model/cbi/ipsec.lua | 122 + .../usr/lib/lua/luci/model/cbi/ipsec.luac | Bin 0 -> 4456 bytes .../usr/lib/lua/luci/model/uci.lua | 508 ++ .../usr/lib/lua/luci/model/uci.luac | Bin 0 -> 10156 bytes .../usr/lib/lua/luci/sgi/cgi.lua | 73 + .../usr/lib/lua/luci/sgi/cgi.luac | Bin 0 -> 1919 bytes .../usr/lib/lua/luci/sgi/uhttpd.lua | 99 + .../usr/lib/lua/luci/sgi/uhttpd.luac | Bin 0 -> 2401 bytes .../usr/lib/lua/luci/store.lua | 6 + .../usr/lib/lua/luci/store.luac | Bin 0 -> 181 bytes .../usr/lib/lua/luci/sys.lua | 615 ++ .../usr/lib/lua/luci/sys.luac | Bin 0 -> 14779 bytes .../usr/lib/lua/luci/sys/zoneinfo.lua | 19 + .../usr/lib/lua/luci/sys/zoneinfo.luac | Bin 0 -> 589 bytes .../usr/lib/lua/luci/sys/zoneinfo/tzdata.lua | 457 ++ .../usr/lib/lua/luci/sys/zoneinfo/tzdata.luac | Bin 0 -> 21092 bytes .../lib/lua/luci/sys/zoneinfo/tzoffset.lua | 46 + .../lib/lua/luci/sys/zoneinfo/tzoffset.luac | Bin 0 -> 1055 bytes .../usr/lib/lua/luci/template.lua | 100 + .../usr/lib/lua/luci/template.luac | Bin 0 -> 2261 bytes .../usr/lib/lua/luci/util.lua | 782 +++ .../usr/lib/lua/luci/util.luac | Bin 0 -> 15174 bytes .../usr/lib/lua/luci/version.lua | 20 + .../usr/lib/lua/luci/version.luac | Bin 0 -> 668 bytes .../usr/lib/lua/luci/xml.lua | 26 + .../usr/lib/lua/luci/xml.luac | Bin 0 -> 501 bytes .../usr/lib/lua/mime.lua | 89 + .../usr/lib/lua/mime.luac | Bin 0 -> 2329 bytes .../usr/lib/lua/mtkwifi.lua | 2090 ++++++ .../usr/lib/lua/mtkwifi.luac | Bin 0 -> 68124 bytes .../usr/lib/lua/nixio/fs.lua | 175 + .../usr/lib/lua/nixio/fs.luac | Bin 0 -> 3706 bytes .../usr/lib/lua/nixio/util.lua | 270 + .../usr/lib/lua/nixio/util.luac | Bin 0 -> 5034 bytes .../usr/lib/lua/shuci.lua | 128 + .../usr/lib/lua/shuci.luac | Bin 0 -> 2733 bytes .../usr/lib/lua/socket.lua | 149 + .../usr/lib/lua/socket.luac | Bin 0 -> 3947 bytes .../usr/lib/lua/socket/ftp.lua | 329 + .../usr/lib/lua/socket/ftp.luac | Bin 0 -> 10592 bytes .../usr/lib/lua/socket/headers.lua | 104 + .../usr/lib/lua/socket/headers.luac | Bin 0 -> 4333 bytes .../usr/lib/lua/socket/http.lua | 420 ++ .../usr/lib/lua/socket/http.luac | Bin 0 -> 10639 bytes .../usr/lib/lua/socket/smtp.lua | 256 + .../usr/lib/lua/socket/smtp.luac | Bin 0 -> 7013 bytes .../usr/lib/lua/socket/tp.lua | 134 + .../usr/lib/lua/socket/tp.luac | Bin 0 -> 3092 bytes .../usr/lib/lua/socket/url.lua | 331 + .../usr/lib/lua/socket/url.luac | Bin 0 -> 5952 bytes .../usr/lib/lua/wps_action.lua | 26 + .../usr/lib/lua/wps_action.luac | Bin 0 -> 673 bytes .../CrossModuleGuardSanitizer.expected | 2 + .../CrossModuleGuardSanitizer.ql | 21 + .../controller.lua | 7 + .../controller.luac | Bin 0 -> 258 bytes .../neutral/sink.lua | 7 + .../neutral/sink.luac | Bin 0 -> 186 bytes .../guard-sanitizer/Q2GuardSanitizer.expected | 2 + .../guard-sanitizer/Q2GuardSanitizer.ql | 14 + .../Q2GuardSanitizerNegatives.expected | 0 .../Q2GuardSanitizerNegatives.ql | 10 + .../library-tests/guard-sanitizer/input.lua | 7 + .../library-tests/guard-sanitizer/input.luac | Bin 0 -> 259 bytes .../InterproceduralModuleTaint.expected | 55 + .../InterproceduralModuleTaint.ql | 219 + ...terproceduralModuleTaintNegatives.expected | 1 + .../InterproceduralModuleTaintNegatives.ql | 70 + .../SAMPLE-MANIFEST.md | 27 + .../controller.lua.txt | 7 + .../controller.luac | Bin 0 -> 349 bytes .../left.lua.txt | 5 + .../left.luac | Bin 0 -> 281 bytes .../missing.lua.txt | 6 + .../missing.luac | Bin 0 -> 316 bytes .../right.lua.txt | 5 + .../right.luac | Bin 0 -> 282 bytes .../bc-branch-negative/input.luac | Bin 0 -> 504 bytes .../bc-kill-overwrite/input.luac | Bin 0 -> 417 bytes .../bc-taint-minimal-path/input.luac | Bin 0 -> 398 bytes .../callsite-balanced-identity/input.luac | Bin 0 -> 194 bytes .../callsite-balanced-identity/source.lua | 10 + .../controller.lua.txt | 14 + .../cross-module-webcmd-popen/controller.luac | Bin 0 -> 677 bytes .../cross-module-webcmd-popen/mtkwifi.lua.txt | 13 + .../cross-module-webcmd-popen/mtkwifi.luac | Bin 0 -> 444 bytes .../controller.lua.txt | 6 + .../controller.luac | Bin 0 -> 345 bytes .../missingfn.lua.txt | 5 + .../missingfn.luac | Bin 0 -> 285 bytes .../controller.lua.txt | 15 + .../controller.luac | Bin 0 -> 798 bytes .../samplelib.lua.txt | 7 + .../samplelib.luac | Bin 0 -> 287 bytes .../same-module-formvalue-execute/input.luac | Bin 0 -> 865 bytes .../source.lua.txt | 22 + .../unresolved-callee-negative/input.luac | Bin 0 -> 731 bytes .../unresolved-callee-negative/source.lua.txt | 15 + .../IntraproceduralNegatives.expected | 1 + .../IntraproceduralNegatives.ql | 91 + .../IntraproceduralSemantics.expected | 16 + .../IntraproceduralSemantics.ql | 198 + .../bc-call-candidate-unresolved/input.luac | Bin 0 -> 444 bytes .../bc-kill-overwrite/input.luac | Bin 0 -> 417 bytes .../bc-table-global-upvalue/input.luac | Bin 0 -> 441 bytes .../bc-table-global-upvalue/source.lua.txt | 9 + .../call-result-table-flow/input.luac | Bin 0 -> 216 bytes .../call-result-table-flow/source.lua | 11 + .../defuse-transitive-chain/input.luac | Bin 0 -> 421 bytes .../defuse-transitive-chain/source.lua.txt | 12 + .../input.luac | Bin 0 -> 538 bytes .../source.lua.txt | 16 + .../input.luac | Bin 0 -> 311 bytes .../source.lua.txt | 9 + .../global-state-write-read/input.lua.txt | 6 + .../global-state-write-read/input.luac | Bin 0 -> 151 bytes .../table-dynamic-key-negative/input.luac | Bin 0 -> 327 bytes .../table-dynamic-key-negative/source.lua.txt | 10 + .../upvalue-mutation-negative/input.luac | Bin 0 -> 364 bytes .../upvalue-mutation-negative/source.lua.txt | 10 + ...idstreamCrossModuleGuardSanitizer.expected | 4 + .../MidstreamCrossModuleGuardSanitizer.ql | 38 + .../neutral/middle.lua | 16 + .../neutral/middle.luac | Bin 0 -> 443 bytes .../neutral/sink.lua | 7 + .../neutral/sink.luac | Bin 0 -> 186 bytes .../source.lua | 8 + .../source.luac | Bin 0 -> 294 bytes .../PathAlternatives.expected | 8 + .../path-alternatives/PathAlternatives.ql | 39 + .../path-alternatives/active_controller.lua | 8 + .../path-alternatives/active_controller.luac | Bin 0 -> 239 bytes .../path-alternatives/active_sink.lua | 10 + .../path-alternatives/active_sink.luac | Bin 0 -> 240 bytes .../path-alternatives/merged_controller.lua | 7 + .../path-alternatives/merged_controller.luac | Bin 0 -> 237 bytes .../path-alternatives/merged_middle.lua | 19 + .../path-alternatives/merged_middle.luac | Bin 0 -> 381 bytes .../path-alternatives/merged_sink.lua | 10 + .../path-alternatives/merged_sink.luac | Bin 0 -> 240 bytes .../path-alternatives/mixed_cleaner.lua | 13 + .../path-alternatives/mixed_cleaner.luac | Bin 0 -> 333 bytes .../path-alternatives/mixed_controller.lua | 13 + .../path-alternatives/mixed_controller.luac | Bin 0 -> 320 bytes .../path-alternatives/mixed_sink.lua | 10 + .../path-alternatives/mixed_sink.luac | Bin 0 -> 240 bytes .../overwrite_controller.lua | 7 + .../overwrite_controller.luac | Bin 0 -> 240 bytes .../path-alternatives/overwrite_middle.lua | 16 + .../path-alternatives/overwrite_middle.luac | Bin 0 -> 349 bytes .../path-alternatives/overwrite_sink.lua | 10 + .../path-alternatives/overwrite_sink.luac | Bin 0 -> 240 bytes .../same_route_controller.lua | 7 + .../same_route_controller.luac | Bin 0 -> 241 bytes .../path-alternatives/same_route_middle.lua | 16 + .../path-alternatives/same_route_middle.luac | Bin 0 -> 385 bytes .../path-alternatives/same_route_sink.lua | 10 + .../path-alternatives/same_route_sink.luac | Bin 0 -> 240 bytes .../path-alternatives/sanitized_cleaner.lua | 12 + .../path-alternatives/sanitized_cleaner.luac | Bin 0 -> 255 bytes .../sanitized_controller.lua | 9 + .../sanitized_controller.luac | Bin 0 -> 304 bytes .../path-alternatives/sanitized_sink.lua | 10 + .../path-alternatives/sanitized_sink.luac | Bin 0 -> 240 bytes .../sibling_return_cleaner.lua | 12 + .../sibling_return_cleaner.luac | Bin 0 -> 542 bytes .../sibling_return_controller.lua | 11 + .../sibling_return_controller.luac | Bin 0 -> 587 bytes .../path-alternatives/sibling_return_echo.lua | 7 + .../sibling_return_echo.luac | Bin 0 -> 235 bytes .../path-alternatives/sibling_return_sink.lua | 10 + .../sibling_return_sink.luac | Bin 0 -> 379 bytes .../SanitizerCallEdge.expected | 5 + .../sanitizer-call-edge/SanitizerCallEdge.ql | 45 + .../sanitizer-call-edge/negative.lua | 6 + .../sanitizer-call-edge/negative.luac | Bin 0 -> 435 bytes .../sanitizer-call-edge/neutral/http.lua | 7 + .../sanitizer-call-edge/neutral/http.luac | Bin 0 -> 224 bytes .../sanitizer-call-edge/neutral/sink.lua | 11 + .../sanitizer-call-edge/neutral/sink.luac | Bin 0 -> 475 bytes .../sanitizer-call-edge/source.lua | 5 + .../sanitizer-call-edge/source.luac | Bin 0 -> 374 bytes .../SourceFileInventory.expected | 3 + .../SourceFileInventory.ql | 5 + .../source-file-inventory/alpha.lua | 2 + .../source-file-inventory/settings.lua | 6 + .../source-file-inventory/worker.lua | 5 + lua/ql/test/qlpack.yml | 7 + lua/tools/autobuild.sh | 16 + lua/tools/corpus_analyzer.py | 3361 ++++++++++ lua/tools/index-files.sh | 7 + lua/tools/index.sh | 7 + lua/tools/index_lua_files.py | 862 +++ lua/tools/lua_bytecode.py | 391 ++ lua/tools/semantic_normalizer.py | 137 + .../tests/fixtures/arithmetic_concat.lua.txt | 8 + .../tests/fixtures/arithmetic_concat.luac | Bin 0 -> 161 bytes lua/tools/tests/fixtures/branch_merge.lua.txt | 9 + lua/tools/tests/fixtures/branch_merge.luac | Bin 0 -> 136 bytes .../tests/fixtures/call_result_table.lua | 11 + .../tests/fixtures/call_result_table.luac | Bin 0 -> 216 bytes .../captured_local_table_field_closure.lua | 11 + .../captured_local_table_field_closure.luac | Bin 0 -> 232 bytes .../captured_local_table_field_overwrite.lua | 12 + .../captured_local_table_field_overwrite.luac | Bin 0 -> 254 bytes lua/tools/tests/fixtures/close_effect.lua.txt | 13 + lua/tools/tests/fixtures/close_effect.luac | Bin 0 -> 208 bytes .../fixtures/conditional_effects.lua.txt | 9 + .../tests/fixtures/conditional_effects.luac | Bin 0 -> 196 bytes .../fixtures/conflicting_literal_require.lua | 9 + .../fixtures/conflicting_literal_require.luac | Bin 0 -> 179 bytes lua/tools/tests/fixtures/fixed_vararg.lua.txt | 6 + lua/tools/tests/fixtures/fixed_vararg.luac | Bin 0 -> 136 bytes lua/tools/tests/fixtures/generic_loop.lua.txt | 9 + lua/tools/tests/fixtures/generic_loop.luac | Bin 0 -> 152 bytes .../tests/fixtures/mixed_fixed_open_call.lua | 17 + .../tests/fixtures/mixed_fixed_open_call.luac | Bin 0 -> 344 bytes .../tests/fixtures/module_global_export.lua | 5 + .../tests/fixtures/module_global_export.luac | Bin 0 -> 177 bytes .../fixtures/module_global_export_alias.lua | 8 + .../fixtures/module_global_export_alias.luac | Bin 0 -> 293 bytes .../fixtures/module_global_table_export.lua | 7 + .../fixtures/module_global_table_export.luac | Bin 0 -> 207 bytes .../module_global_table_replacement.lua | 18 + .../module_global_table_replacement.luac | Bin 0 -> 361 bytes .../module_global_table_seeall_export.lua | 7 + .../module_global_table_seeall_export.luac | Bin 0 -> 248 bytes .../fixtures/module_missing_field_caller.lua | 3 + .../fixtures/module_missing_field_caller.luac | Bin 0 -> 157 bytes .../module_multiple_field_exports.lua | 9 + .../module_multiple_field_exports.luac | Bin 0 -> 240 bytes .../module_multiple_same_module_exports.lua | 13 + .../module_multiple_same_module_exports.luac | Bin 0 -> 357 bytes ...tiple_same_module_global_table_exports.lua | 15 + ...iple_same_module_global_table_exports.luac | Bin 0 -> 407 bytes .../fixtures/module_nested_field_caller.lua | 14 + .../fixtures/module_nested_field_caller.luac | Bin 0 -> 913 bytes .../fixtures/module_nested_field_provider.lua | 7 + .../module_nested_field_provider.luac | Bin 0 -> 381 bytes .../module_same_module_export_call.lua | 9 + .../module_same_module_export_call.luac | Bin 0 -> 284 bytes .../module_same_module_global_table_call.lua | 11 + .../module_same_module_global_table_call.luac | Bin 0 -> 330 bytes .../tests/fixtures/module_seeall_export.lua | 5 + .../tests/fixtures/module_seeall_export.luac | Bin 0 -> 218 bytes lua/tools/tests/fixtures/numeric_loop.lua.txt | 9 + lua/tools/tests/fixtures/numeric_loop.luac | Bin 0 -> 157 bytes lua/tools/tests/fixtures/open_call.lua.txt | 6 + lua/tools/tests/fixtures/open_call.luac | Bin 0 -> 140 bytes .../tests/fixtures/open_vararg_return.lua.txt | 5 + .../tests/fixtures/open_vararg_return.luac | Bin 0 -> 124 bytes lua/tools/tests/fixtures/returned_closure.lua | 5 + .../tests/fixtures/returned_closure.luac | Bin 0 -> 120 bytes .../tests/fixtures/returned_table_alias.lua | 8 + .../tests/fixtures/returned_table_alias.luac | Bin 0 -> 145 bytes .../fixtures/returned_table_replacement.lua | 18 + .../fixtures/returned_table_replacement.luac | Bin 0 -> 354 bytes lua/tools/tests/fixtures/scalar_range.lua.txt | 7 + lua/tools/tests/fixtures/scalar_range.luac | Bin 0 -> 144 bytes .../fixtures/state_access_effects.lua.txt | 15 + .../tests/fixtures/state_access_effects.luac | Bin 0 -> 215 bytes .../stripped_literal_require_member_call.lua | 3 + .../stripped_literal_require_member_call.luac | Bin 0 -> 159 bytes ...ripped_literal_require_sibling_capture.lua | 11 + ...ipped_literal_require_sibling_capture.luac | Bin 0 -> 303 bytes ...tripped_literal_require_upvalue_member.lua | 7 + ...ripped_literal_require_upvalue_member.luac | Bin 0 -> 235 bytes .../stripped_nested_getupval_capture.lua | 11 + .../stripped_nested_getupval_capture.luac | Bin 0 -> 303 bytes .../fixtures/table_alias_replacement.lua.txt | 12 + .../fixtures/table_alias_replacement.luac | Bin 0 -> 175 bytes .../tests/fixtures/unary_effects.lua.txt | 8 + lua/tools/tests/fixtures/unary_effects.luac | Bin 0 -> 144 bytes .../tests/fixtures/upvalue_member_call.lua | 11 + .../tests/fixtures/upvalue_member_call.luac | Bin 0 -> 442 bytes .../upvalue_member_call_stripped.luac | Bin 0 -> 259 bytes lua/tools/tests/test_corpus_analyzer.py | 5636 +++++++++++++++++ lua/tools/tests/test_extractor_trap.py | 104 + lua/tools/tests/test_qhelp_examples.py | 30 + lua/tools/tests/test_source_archive.py | 55 + lua/tools/trap_writer.py | 48 + misc/scripts/create-change-note.py | 1 + .../generate-code-scanning-query-list.py | 8 +- 434 files changed, 33115 insertions(+), 5 deletions(-) create mode 100644 lua/README.md create mode 100644 lua/SCHEMA.md create mode 100644 lua/codeql-extractor.yml create mode 100644 lua/lua.dbscheme create mode 100644 lua/lua.dbscheme.stats create mode 100644 lua/ql/lib/change-notes/2026-07-21-lua-bytecode-analysis.md create mode 100644 lua/ql/lib/codeql/lua/Bytecode.qll create mode 100644 lua/ql/lib/codeql/lua/InterproceduralModuleTaint.qll create mode 100644 lua/ql/lib/codeql/lua/IntraproceduralSemantics.qll create mode 100644 lua/ql/lib/codeql/lua/RulesSanitizerReport.qll create mode 100644 lua/ql/lib/codeql/lua/SourceFile.qll create mode 100644 lua/ql/lib/lua.dbscheme create mode 100644 lua/ql/lib/lua.dbscheme.stats create mode 100644 lua/ql/lib/lua.qll create mode 100644 lua/ql/lib/qlpack.yml create mode 100644 lua/ql/src/README.md create mode 100644 lua/ql/src/change-notes/2026-07-21-lua-bytecode-queries.md create mode 100644 lua/ql/src/codeql-suites/lua-code-quality-extended.qls create mode 100644 lua/ql/src/codeql-suites/lua-code-quality.qls create mode 100644 lua/ql/src/codeql-suites/lua-code-scanning.qls create mode 100644 lua/ql/src/codeql-suites/lua-security-and-quality.qls create mode 100644 lua/ql/src/codeql-suites/lua-security-experimental.qls create mode 100644 lua/ql/src/codeql-suites/lua-security-extended.qls create mode 100644 lua/ql/src/experimental/Diagnostics/LuaBytecodeFacts.ql create mode 100644 lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.qhelp create mode 100644 lua/ql/src/experimental/Diagnostics/SanitizedCommandFlow.ql create mode 100644 lua/ql/src/experimental/Diagnostics/examples/SanitizedCommandFlow.lua create mode 100644 lua/ql/src/experimental/README.md create mode 100644 lua/ql/src/experimental/Security/CWE-078/CommandInjection.qhelp create mode 100644 lua/ql/src/experimental/Security/CWE-078/CommandInjection.ql create mode 100644 lua/ql/src/experimental/Security/CWE-078/examples/CommandInjection.lua create mode 100644 lua/ql/src/qlpack.yml create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.expected create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.lua create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.luac create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/CommandInjection.qlref create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/README.md create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.expected create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.lua create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.luac create mode 100644 lua/ql/test/experimental/query-tests/qhelp-examples/SanitizedCommandFlow.qlref create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.expected create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/CommandInjection.qlref create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.expected create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/LuaBytecodeSanitizedPath.qlref create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.expected create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReport.ql create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.expected create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/RulesSanitizerReportNegatives.ql create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/SAMPLE-MANIFEST.md create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.expected create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/TableFieldSanitizerOverwrite.ql create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-branch-negative/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-kill-overwrite/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/bc-taint-minimal-path/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/callsite-balanced-report/source.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/constant-sink-overmatch-negative/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/controller.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-sanitizer/sanitizer.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/controller.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/cross-module-webcmd-popen/mtkwifi.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/formvalue-os-execute-chain/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/no-report-without-path-negative/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/controller.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-cross-module-return/shellsafe.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-on-path/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-same-suffix-off-chain-negative/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/sanitizer-unsanitized-alternative/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/input.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/submit-dpp-uri-execute/source.lua.txt create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/dynamic-key.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/optional-branch.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/same-field.luac create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.lua create mode 100644 lua/ql/test/experimental/query-tests/rules-sanitizer-report/table-field-sanitizer-overwrite/unrelated-field.luac create mode 100644 lua/ql/test/library-tests/README.md create mode 100644 lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.expected create mode 100644 lua/ql/test/library-tests/bytecode-model/BytecodeDiagnostics.ql create mode 100644 lua/ql/test/library-tests/bytecode-model/BytecodeModel.expected create mode 100644 lua/ql/test/library-tests/bytecode-model/BytecodeModel.ql create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-constants-call/input.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/truncated.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-prototype-params/input.luac create mode 100644 lua/ql/test/library-tests/bytecode-model/bc-stripped-metadata/input.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/.gitattributes create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.expected create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/CorpusAcceptance.ql create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/SAMPLE-MANIFEST.md create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/SHA256SUMS create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/mtwifi.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/quick_setting.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/lib/wifi/wifi_services_sh.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/l1dat_parser.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/ltn12.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/cacheloader.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ccache.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/config.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/index.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/admin/uci.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/hwnat.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/ipsec.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/controller/mtkwifi.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/debug.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/dispatcher.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/http.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/i18n.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/ltn12.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/hwnat.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/cbi/ipsec.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/model/uci.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/cgi.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sgi/uhttpd.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/store.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/template.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/util.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/version.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/luci/xml.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mime.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/mtkwifi.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/fs.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/nixio/util.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/shuci.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/ftp.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/headers.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/http.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/smtp.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/tp.luac create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/socket/url.luac create mode 100755 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.lua create mode 100644 lua/ql/test/library-tests/complex-bytecode-integration/usr/lib/lua/wps_action.luac create mode 100644 lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.expected create mode 100644 lua/ql/test/library-tests/cross-module-guard-sanitizer/CrossModuleGuardSanitizer.ql create mode 100644 lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.lua create mode 100644 lua/ql/test/library-tests/cross-module-guard-sanitizer/controller.luac create mode 100644 lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.lua create mode 100644 lua/ql/test/library-tests/cross-module-guard-sanitizer/neutral/sink.luac create mode 100644 lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.expected create mode 100644 lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizer.ql create mode 100644 lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.expected create mode 100644 lua/ql/test/library-tests/guard-sanitizer/Q2GuardSanitizerNegatives.ql create mode 100644 lua/ql/test/library-tests/guard-sanitizer/input.lua create mode 100644 lua/ql/test/library-tests/guard-sanitizer/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.expected create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaint.ql create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.expected create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/InterproceduralModuleTaintNegatives.ql create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/SAMPLE-MANIFEST.md create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/controller.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/left.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/missing.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/ambiguous-unresolved-dynamic-module-negative/right.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/bc-branch-negative/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/bc-kill-overwrite/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/bc-taint-minimal-path/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/callsite-balanced-identity/source.lua create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/controller.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/cross-module-webcmd-popen/mtkwifi.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/controller.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-missing-field-negative/missingfn.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/controller.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/module-return-table-field-call/samplelib.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/same-module-formvalue-execute/source.lua.txt create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/input.luac create mode 100644 lua/ql/test/library-tests/interprocedural-module-taint/unresolved-callee-negative/source.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.expected create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralNegatives.ql create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.expected create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/IntraproceduralSemantics.ql create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/bc-kill-overwrite/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/bc-table-global-upvalue/source.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/call-result-table-flow/source.lua create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/defuse-transitive-chain/source.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/defuse-unrelated-register-negative/source.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/global-dynamic-environment-negative/source.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/global-state-write-read/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/table-dynamic-key-negative/source.lua.txt create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/input.luac create mode 100644 lua/ql/test/library-tests/intraprocedural-semantics/upvalue-mutation-negative/source.lua.txt create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.expected create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/MidstreamCrossModuleGuardSanitizer.ql create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.lua create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/middle.luac create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.lua create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/neutral/sink.luac create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.lua create mode 100644 lua/ql/test/library-tests/midstream-cross-module-guard-sanitizer/source.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/PathAlternatives.expected create mode 100644 lua/ql/test/library-tests/path-alternatives/PathAlternatives.ql create mode 100644 lua/ql/test/library-tests/path-alternatives/active_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/active_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/active_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/active_sink.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/merged_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/merged_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/merged_middle.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/merged_middle.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/merged_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/merged_sink.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/mixed_cleaner.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/mixed_cleaner.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/mixed_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/mixed_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/mixed_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/mixed_sink.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/overwrite_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/overwrite_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/overwrite_middle.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/overwrite_middle.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/overwrite_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/overwrite_sink.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/same_route_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/same_route_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/same_route_middle.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/same_route_middle.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/same_route_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/same_route_sink.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sanitized_cleaner.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sanitized_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sanitized_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sanitized_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sanitized_sink.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_cleaner.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_controller.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_controller.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_echo.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_echo.luac create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_sink.lua create mode 100644 lua/ql/test/library-tests/path-alternatives/sibling_return_sink.luac create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.expected create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/SanitizerCallEdge.ql create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/negative.lua create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/negative.luac create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.lua create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/neutral/http.luac create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.lua create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/neutral/sink.luac create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/source.lua create mode 100644 lua/ql/test/library-tests/sanitizer-call-edge/source.luac create mode 100644 lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.expected create mode 100644 lua/ql/test/library-tests/source-file-inventory/SourceFileInventory.ql create mode 100644 lua/ql/test/library-tests/source-file-inventory/alpha.lua create mode 100644 lua/ql/test/library-tests/source-file-inventory/settings.lua create mode 100644 lua/ql/test/library-tests/source-file-inventory/worker.lua create mode 100644 lua/ql/test/qlpack.yml create mode 100755 lua/tools/autobuild.sh create mode 100644 lua/tools/corpus_analyzer.py create mode 100755 lua/tools/index-files.sh create mode 100755 lua/tools/index.sh create mode 100755 lua/tools/index_lua_files.py create mode 100644 lua/tools/lua_bytecode.py create mode 100644 lua/tools/semantic_normalizer.py create mode 100644 lua/tools/tests/fixtures/arithmetic_concat.lua.txt create mode 100644 lua/tools/tests/fixtures/arithmetic_concat.luac create mode 100644 lua/tools/tests/fixtures/branch_merge.lua.txt create mode 100644 lua/tools/tests/fixtures/branch_merge.luac create mode 100644 lua/tools/tests/fixtures/call_result_table.lua create mode 100644 lua/tools/tests/fixtures/call_result_table.luac create mode 100644 lua/tools/tests/fixtures/captured_local_table_field_closure.lua create mode 100644 lua/tools/tests/fixtures/captured_local_table_field_closure.luac create mode 100644 lua/tools/tests/fixtures/captured_local_table_field_overwrite.lua create mode 100644 lua/tools/tests/fixtures/captured_local_table_field_overwrite.luac create mode 100644 lua/tools/tests/fixtures/close_effect.lua.txt create mode 100644 lua/tools/tests/fixtures/close_effect.luac create mode 100644 lua/tools/tests/fixtures/conditional_effects.lua.txt create mode 100644 lua/tools/tests/fixtures/conditional_effects.luac create mode 100644 lua/tools/tests/fixtures/conflicting_literal_require.lua create mode 100644 lua/tools/tests/fixtures/conflicting_literal_require.luac create mode 100644 lua/tools/tests/fixtures/fixed_vararg.lua.txt create mode 100644 lua/tools/tests/fixtures/fixed_vararg.luac create mode 100644 lua/tools/tests/fixtures/generic_loop.lua.txt create mode 100644 lua/tools/tests/fixtures/generic_loop.luac create mode 100644 lua/tools/tests/fixtures/mixed_fixed_open_call.lua create mode 100644 lua/tools/tests/fixtures/mixed_fixed_open_call.luac create mode 100644 lua/tools/tests/fixtures/module_global_export.lua create mode 100644 lua/tools/tests/fixtures/module_global_export.luac create mode 100644 lua/tools/tests/fixtures/module_global_export_alias.lua create mode 100644 lua/tools/tests/fixtures/module_global_export_alias.luac create mode 100644 lua/tools/tests/fixtures/module_global_table_export.lua create mode 100644 lua/tools/tests/fixtures/module_global_table_export.luac create mode 100644 lua/tools/tests/fixtures/module_global_table_replacement.lua create mode 100644 lua/tools/tests/fixtures/module_global_table_replacement.luac create mode 100644 lua/tools/tests/fixtures/module_global_table_seeall_export.lua create mode 100644 lua/tools/tests/fixtures/module_global_table_seeall_export.luac create mode 100644 lua/tools/tests/fixtures/module_missing_field_caller.lua create mode 100644 lua/tools/tests/fixtures/module_missing_field_caller.luac create mode 100644 lua/tools/tests/fixtures/module_multiple_field_exports.lua create mode 100644 lua/tools/tests/fixtures/module_multiple_field_exports.luac create mode 100644 lua/tools/tests/fixtures/module_multiple_same_module_exports.lua create mode 100644 lua/tools/tests/fixtures/module_multiple_same_module_exports.luac create mode 100644 lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.lua create mode 100644 lua/tools/tests/fixtures/module_multiple_same_module_global_table_exports.luac create mode 100644 lua/tools/tests/fixtures/module_nested_field_caller.lua create mode 100644 lua/tools/tests/fixtures/module_nested_field_caller.luac create mode 100644 lua/tools/tests/fixtures/module_nested_field_provider.lua create mode 100644 lua/tools/tests/fixtures/module_nested_field_provider.luac create mode 100644 lua/tools/tests/fixtures/module_same_module_export_call.lua create mode 100644 lua/tools/tests/fixtures/module_same_module_export_call.luac create mode 100644 lua/tools/tests/fixtures/module_same_module_global_table_call.lua create mode 100644 lua/tools/tests/fixtures/module_same_module_global_table_call.luac create mode 100644 lua/tools/tests/fixtures/module_seeall_export.lua create mode 100644 lua/tools/tests/fixtures/module_seeall_export.luac create mode 100644 lua/tools/tests/fixtures/numeric_loop.lua.txt create mode 100644 lua/tools/tests/fixtures/numeric_loop.luac create mode 100644 lua/tools/tests/fixtures/open_call.lua.txt create mode 100644 lua/tools/tests/fixtures/open_call.luac create mode 100644 lua/tools/tests/fixtures/open_vararg_return.lua.txt create mode 100644 lua/tools/tests/fixtures/open_vararg_return.luac create mode 100644 lua/tools/tests/fixtures/returned_closure.lua create mode 100644 lua/tools/tests/fixtures/returned_closure.luac create mode 100644 lua/tools/tests/fixtures/returned_table_alias.lua create mode 100644 lua/tools/tests/fixtures/returned_table_alias.luac create mode 100644 lua/tools/tests/fixtures/returned_table_replacement.lua create mode 100644 lua/tools/tests/fixtures/returned_table_replacement.luac create mode 100644 lua/tools/tests/fixtures/scalar_range.lua.txt create mode 100644 lua/tools/tests/fixtures/scalar_range.luac create mode 100644 lua/tools/tests/fixtures/state_access_effects.lua.txt create mode 100644 lua/tools/tests/fixtures/state_access_effects.luac create mode 100644 lua/tools/tests/fixtures/stripped_literal_require_member_call.lua create mode 100644 lua/tools/tests/fixtures/stripped_literal_require_member_call.luac create mode 100644 lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.lua create mode 100644 lua/tools/tests/fixtures/stripped_literal_require_sibling_capture.luac create mode 100644 lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.lua create mode 100644 lua/tools/tests/fixtures/stripped_literal_require_upvalue_member.luac create mode 100644 lua/tools/tests/fixtures/stripped_nested_getupval_capture.lua create mode 100644 lua/tools/tests/fixtures/stripped_nested_getupval_capture.luac create mode 100644 lua/tools/tests/fixtures/table_alias_replacement.lua.txt create mode 100644 lua/tools/tests/fixtures/table_alias_replacement.luac create mode 100644 lua/tools/tests/fixtures/unary_effects.lua.txt create mode 100644 lua/tools/tests/fixtures/unary_effects.luac create mode 100644 lua/tools/tests/fixtures/upvalue_member_call.lua create mode 100644 lua/tools/tests/fixtures/upvalue_member_call.luac create mode 100644 lua/tools/tests/fixtures/upvalue_member_call_stripped.luac create mode 100644 lua/tools/tests/test_corpus_analyzer.py create mode 100644 lua/tools/tests/test_extractor_trap.py create mode 100644 lua/tools/tests/test_qhelp_examples.py create mode 100644 lua/tools/tests/test_source_archive.py create mode 100644 lua/tools/trap_writer.py 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 0000000000000000000000000000000000000000..4f497bed203e779cafcd065f8616498d45b7abb3 GIT binary patch literal 366 zcmZvYO^O0B5QSfj@fUX^x=^H#5(NbZH|}Q>nurZ*H{FT2jG*4Yji+fBdI}FSCDT7J zPK6NaeeYFB`g5xmAkh#Fyw^vy$6}RtO-j_9#3%&@4m1H_DsMPA0!yG9;R2#>3Hoxt zOpcGUbbLC);mW+5!reLZkZk1jWZ#fko0)iB@T#@-gR)y06G60o5td=R*{QYF5cNz_ zz6ruDL?!FP1jQ@#Ui;RtxtYO4$GgDSUa5GoPK=68IgndpdC2FVjEW*JXZbf3;m$wi J9A>CEegGh&J 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 0000000000000000000000000000000000000000..29c4dfe7ad77757ed7b4a4b10c241ee55a80616f GIT binary patch literal 504 zcmZ8d%T5C^40M)I3M!9NITZE8wcTF`cP@NFx^?NQRVr<=sK9HQul5&{9KKOdm9z)-;uUE}t)mhhU7mhz|Xur7jhrVlgx$kZ10aT+$ zsjrf2Llgy1G15iw*%&oFi=-kM71hfdscHPn=Rzy->T4+oZ#PVOlnEWemb+vq0x3CM zWl{mb=K{D0XZcZ%U|Sb>od}GWf-KER9}0%wfGDcq87_o`fQBc*gCXFEW2#UP*aL2d z>$c>b;H2ce;O~Tg$R{~#$@B`ryy`UULFmtRTDMEnA^!g<`k@cYYxZniYK4mU2Et4+ A8vpE~le%-N4Ge`C0-KtH zp4^<1C5WI5HEAFu02)?84K4%+9>GHa@Ip*S6>`{`ZS!ZThGmMM6^3GQECN{FUBfL9 O(`VeI@rrU*(i^|Yh9{N) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71021c22082e4152557d773429eff344edeacbac GIT binary patch literal 398 zcmYjNOAdlC6nsTc^a>`f+#>M?9Kel7s8C}gprNf{$w54f2k|hzX=&ml?>C+INw+5_ z&m_`3&FM%1l~u-?@~TJc24%`iU0A7mTXeeDootJzw71ec+)%4S>*U;qkYqXgJzG-| zO&s6TL?2{=#J3``^DHa*Ekbaz2t|U3ddC4;NKlC4veat0Ro E0g#0zMgRZ+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7553128e3e316aacf4c1a4aec81a689e7e9338cb GIT binary patch literal 216 zcmb34DNPJyU}WK7;b6c7Ol*8WK@}i&1>%^728IS8eE>+#X<%SH0Aw>dGBDf$vRNA$ z7#tWI7-bw880P_n<$!9Kf$}WuKmr7c^Gl18QyEy;AY6vzoYcfT2Dq)M6{*RkC8-RI ZP-zeaVzWRPOdzuyfH)3nHb@vB002}`6qW!0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5395e574a43fa88198b28798a259df9c39c4669c GIT binary patch literal 339 zcmZut%L>9U5S-{*Dx!E-`~&+9@t`L!-o1o2#X=vViS}6k%>J3#Hlm0FlVoOgvmuMU zRR;(PDa0H-F zjo!=~&+qEqXAclFG(-GHlHO1pTVPHe*$@Wgl-9QvMXO;qCJ!QYT4D7c-cu`>2ElcJ z+%Z415wj4ni-JdLd)y)-;6VSAbzk@p@O@jzg!+gzMh7V>pO=5pbJM)Q=upMes$Oie z$~&0eQNe1B-)r2~<-TFEJFj-HzN{&94c6JRY|1xR!y8r+H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..494e02b445a4905aebe13e2121934d7e37a1e084 GIT binary patch literal 677 zcmb7B%TB{E5FDpzY0E<(aRc=eD&PxTiHB5i;SNM04-qvFh-}I+{8{-kcAP|16&FSt zk3F{6yX&)i8$Lk!V}FcOw9rvin!3vKq@L$CoSRS6?NB`LM8>HGmZB5gG<%{$;Ye%5 z&coMm11z9$-ay4#Ou?O2xDh_;1~W7miRbeok=rQyUA-Ff27lUaA+74-Bg}0A$+AVA zyxXkaG77U=XQ_l>HpDu4iHaCryK>_|M)#hQDr7zb6cgz|&lmEH>Qn?}SR_Qe&1_mK!2O;ao=YuzoRD~BH6a1 ME&dgoNgfM{AL>^{pa1{> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7fa65fafbfd53ef6ca1db619fd1d13128b29fe4a GIT binary patch literal 444 zcmaKoK~BR!3`PGW1EfVPSR?fa6>x%VXa#}=2Y@I_S}7`-bdjiChP!YOj)q^FNQebr z`s0~zkNxGF=PrMh^NCM%trOC^-5siJwfyMvWqWK#nGnS@JLkryRQj^`*!TU~VrlPL zs?UYw7U~t7<~cys$e*9C+Bf@6ke`NayHI3Z*oEeh-DY?9ipDyEHxB=tBJ`;_Ie>O% zq{bPk2Sr^D5$!iHe~I7}^XI|1O25sn^k(Y2Ma)ZghqK!#uDtaKSjX{?^yffbk z8-tJO=Pq9-gDazOnV_!4zrhz0fCmG$kf@eevl-Dx`?C9b4&fDgKBn+Ato>WtM+ZGA z*06#6!3ei?HU>%RVp!jyDlvtd@;M3>{RPGWm?P+MFKbj$5v*G8sM*XxS$J|D?w^KD zsbRzY!PtTx!P{lLPY(MtFqJpXv&ci?(H}e(?>E_#wS*nKZ&%%8aPqvTVw`0g_y%6h BD=Yv2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..625e984ffefa27a982637108bb918d9209764b80 GIT binary patch literal 567 zcmaJ-OHRWu6dVVWHbs0FumJT4J%h7FY&e3I7*Q!7)Y_;^g9NAGFg+S?5?3u!#goQk z&sgKDBu;=-FuDrC+u0wh0dU zBuV`m@{6(D##O}!|_T_!89w4G&G{hM?lW~#TvM@%MGgGUXU0d&A>h-VaKbF89@kln!fVfZC zmlBE7uo)8%TG9!LAZ=1q%DEAU9#8*b2|!8@%#Dens(0`z@L zh$-nK%89rUo2ldUZ@0Ae2@yACTNGvXtW2#D?R5%@3UtEG^jgQYC1(&yy{glagixSb zYGW$3ASv=mah4!D@Ehk})ddyld5MXex%mWotWVJm&OSEQxE{v{&qT;orkS1S7XHU4 zb~8~yPT*Sz#(VZLhl9jLmZ>}mA&&XAIA$9>Xc(56Uyu>{F&}Q>h}OF}CN1EE*1O=k g|L^mZ)CnO>Y*m_Do#5Q-zZoh|ZTdo<3-2Mq56*B%>i_@% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..468b5c7f052867081035cc66c06fd6f85b45ff1c GIT binary patch literal 235 zcmXv|Ne;p=5OYF+LXRAg`h>or4}b$th(u5ikq~N9_1LFlC#@yV;+e7L{>c6X;~LlC zhKgYtLyVLEgg$29rzLFzC2Nn2SMp6>2CN-mI;=44UCSU8i(mexQ1mJS2Nt@gXf4xz jD@l;%NbSh)L<+NyIh0DOux}{K^}JDYxd_ktQ*H1E)%X%` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ed5aa4a9e65ce6685a83495a022519ce7913710e GIT binary patch literal 687 zcmaJ;yG{c!5FCd~NWvRL0Z6H+^9^nY0@2XZ9Ct>XbPsf~QAFkN9h7{?QNUO5G3?w! zLFRskH+-8aAjnssSBreVOy52*FTkctbuFsBkldB_`YDE zHR77X@?489c=y1S$LnPUTmYEq3Nv3}sTJ-#l*p*y@h0)7Z0U#^=R!Af^j3^p(wNGFm(pOMoSfQNJovCg^8RMF=&Y>i_s;LVqBl2(CbM&L306W zG;-X_9MyVp$ivLg*JK^E6JKS-cuhgxjyWvIPH9aOjL5Fqwk&o@tf{!u=5X(7tIamf z0RNBrD%gJa1oXVsmVoCGdxC?1a41}dBZ>c_t0fX_k-O~Ply&hFA-d18VUk-D*M-P; Re>xBI#3nDI91BZ@_5;agJW~Jw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..893ad75a87cccb1a8dee9b036bc4fcafceaca86c GIT binary patch literal 706 zcmaJ;NlpVX5G)5LBw=4pM9PVC-rx&CATHdw3=>C0ngt~`%F*F1JR}_86+DKnnX!o2 zS}J#UwWqp!Jh<`6EtKx+E)G$fS(JXB+EM0{kz2ZkrE9eZa`p{yExu<$*cRUwbd15I z;;=e5QWv}g;40wxat&Mn7@IZ5VU3khxC>CLpdckcL46pgH%Z>{Dp>&YAp)qc*?^Y$ zboP}!IQIzMuS=Mg^ZY5vyoKIssB!_>sy4f{sdrM;4Uy&(=uVSNXG=Rl{Ej5!s;xq+ zCGk+EhF|jQ8{r z8e+WU^xQf3&V968KIHx*l-Kb(I6`T9X7Xte4U^m-+PSS%QmcI}&z=VEg}d6hJ>j-s zS8IfYhQ*C8@u$GvEQ+>b* zV~Gd?F+nK8N#!frf+_dNAPwk$;==dmln;VAh#0q zDj`V2&E>#kVQgcQp}xy3`bBGnR!jVWOwE~UqsDT~Q56dz^V5i2h=o%Uaxmn)Y0(E%0J_t!~2p_zCyGodrUzFcYk_#TpiK009ru3X|lA*+X@;yoy){O!jySg2!)+njr%>YvQQ6F#iRapWxwjaf&Yj(2sj9J yQeCOTHF_a@3<^I68(?F{7qA(MLE|sto-z13!wZr5vnT>_CSYC)bTfju&i(*77e6}y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e67b75bab37063d4987164fbb5568dcfdb77cd07 GIT binary patch literal 343 zcmZutv2Md46g(Qz+S%G6OQrll^9?NCx^#^!MHHjTN(_<>QoXUXU(t`rl<&%W#Zf%u zq=Wa4gLmTfJ?ak#S;zv<$WYDbbk~}Hq)$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fcf7b3724f8e191c5300e87384de8165de3a1a60 GIT binary patch literal 314 zcmYjLyAHxI5OZjFF0jCY#2>&nWMN}rMJ#EEM5Q)CZ6Yyd;=A}X?wU%}l9M|-Ip6*g z^%X)cjfO|?hYC@TN} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a151c81a0dd2cf356b638c636a7e49df3eee501 GIT binary patch literal 369 zcmZ9Hu?~VT5QhIsRnWyjSxkHbeTFVEu18A(PQv=DJS*fkEyv=O|8TACMtz3CEHRN(2 z&D|8m!`YTDeqT0ElR5${{)!+6BTI_Mo>KmA9%^i!U~GPqzr$);P2HHRKRNIQzy2;0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eaf1036ac9355e4e69004162694dd6e5e00943b0 GIT binary patch literal 389 zcmZ8dyAHxI47@;i6joqC;t${(vUFo%MJ%NaqEecOHjx;^!guj$*tAH6lTNYEj(y2` zAF>04oXRPdXrY3wwaL6LwwF?yVrN3umG~G8xp78J6>y3Xi9$?c8y`tuLBtdSPbjKG zBq}~uFX~ahS{l1tL2M8)p}XXxMeeF9vjrrpjK+mJ*O0uKkvm(IzH}CH+$wPXs%yxk zOTDw9IpYOa;Fk&Fut!O8pCiiu$sYS;2YvF)ugS*K*M7Dv HNM*bM31%}U literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..829baa29a56c7c303619ca9176d92322153bac74 GIT binary patch literal 348 zcmaJ*OAdlC5S=0-x_6~v^T($rwmfT3-|5^v^3^eoOR5?%R{*O_^J^Vyxe zJd=piIHjB>lulb`i_3LzzSp*>Rj#_$I@vm#D``w&JKrmPG+usM5Tazm?}1q{? nTn5LIiKVMQoPBH|t(q>3U2`#CYhIhv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6d7172f904f009ff70725abd0e127bb3529f7cd5 GIT binary patch literal 398 zcmYjNOAdlC6nsTc@CqiZ+#>M?9Kel7s8C}gprNf{$w54f2k|hzX(4fv_nXf9q`Q-o zXA*H5r*x!%$|_?`dDWwJgEHl%F09nOEjr!nPPWBU+FNNJZm89vb#iV)h?123o~@~f zCXVlEq7O1b;#-l(J5G^`f+#>M?9Kel7s8C}gprNf{$w54f2k|hzX=&ml?>C+INw+5_ z&qAbmn$wX2Dyxh&X-@xS>{u*2%dIA<1&~d$y(` znmE3vi9W~#iEl+>=UG{&Te3leC8&YXdoTbtV>(pp*)|;iExt2N;aB`XwBQK9=>G0EfW%hdC0nN``f+#>M?9Ke+~s8C}gprNf{$w54f2k|hzX=&ml?>C+INw+5_ z&m_`3&FM%1l~u-?@~TJc24%`iU0A7mTXeeDootJzw71ec+)%4S>*U;qkYqXgJzG-| zO&s6TL?2{=#J3``^DHa*Ekbaz2t|U3ddC4;NKlC4veat0Ro E0g?|UM*si- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8bce7bc47c7e50a7cb24a62d3d9b8d1380584c06 GIT binary patch literal 340 zcmZ`!OA5j;6nv>sdk4Xl+nOu%0B$^hLRunJ)YK*c-PJ?5^(^&fzG)1u9GEx5%ll=q z33^LH<}xQm5v4cIJ9TJoK6J*ZsxI2jdh744DO%m>vs2E7t~Tpa(BCFVCg&Vi%#!(F zzS-=k7klM3KD2tfO@R}wKdMtBLAk({#h%z@)`An_s+n3RBb3{0G`M=vxjiZ>#g fG8Ow+mooL(ff@k+fS>&#F2rWwXq(G;K8k@4OeP>l literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7cd65439088b9a61b4c11a8e56d3e529ea526ff0 GIT binary patch literal 154 zcmb34DNPJyU}WK7;b6c7Of2j`K}H}>0pbQAJOHHU0)^%QX*nPtB*(%DBtRfHwInem eu_O^J3K3#pU + +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 0000000000000000000000000000000000000000..317737bc021773b97dc350d74ad9ea3cb8aa633e GIT binary patch literal 11598 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6419c9715a47e97dcf0a7d4d7132ae81b8b3adb4 GIT binary patch literal 28098 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a27d76fca4339a340c75a70fadf488089007acbb GIT binary patch literal 5752 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 w)-scqDlE>-o5EjRO%W6G|8maGY0tbeJ&!x + * + * 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 0000000000000000000000000000000000000000..880a6484c872c8ad815ce70b1cb0bb15fb290d66 GIT binary patch literal 7015 zcmdT}&2JmW6@Rn4T$1{-3ku#e=1ugZNMiQPiI~kKb}mz z(OoP*UChqpGUfc#dC~@bx@77{0pYL@o65b_LpkC@kC|^|j=z2^W4@d?I!>ww{rign zuM&SE`KrY3G&H%U#2!n%ilf}QX6lXC#?x}vvxc_ojy;p{WFiwck0&J!8Nms1=c^g- zXry`K$K+t&?CfMAAK-O=T^HIF!nvtRK%)61fOSPTldHTLP=tI60VFN#k20BRIX}}~ zGn1K|K69v$ukb9LOklk()?2X$C0CR}4mK-g zr!s;S&;GibO_p=nYNn7cp3O{`%9-L@TJpQ+59?&|l{|&lEaGp8zCghjkSRk{${fIV&KYN_$A6<(f;iSz&}Q3 z8Vx^-r)#QU`6S+jdE7GaB;rEYBK+vgP?C72O=vpHQ*rtNY4n2OLOlGk-bIiEV zwJN+uDDDAuEsgb#tFR|{2l+rxxC*lPZP%leWbZvHU1ob?Eh&35X&GXBlV_Z#Z~G&l zM@1xI^u+J+Okpg--E{-mBF39k=M$W>CMRmy$Tm2!>%fOFK{rjstTQ(c>b-7Lz@=L) zg=2n;DKt?>NHd`T@~-Q5%r(W$TIdnJfP2hp#6z~iwPl1sgvn5IO(9E31h5XqjeIA)aavR zGPGACWeb*sef!c~c4;^hG^Fxy2Map$?J#LPWVSWhTIYxOF%LUAu`(`;^5`=g@wpgf-^z)NRo6RABLc}scPYv5#`(V zOzN~X&zab--#(kFdc{%a*{uZjf}yt(8oBQVf*Ig9z|W?w>S8Zln`fKq!j%{f=ET9% zJa|&Wa2N12Kd*1$ni2e3z_l77Vu>(eUu}0n)t7<0=IZiqwFT(csvlNuh6Dkr)g_GE z=uT@x)u0Dm8Pk$@rsE%I^9n_dAcBq?3##|jwA4aG$blv_ah1NvAsVteV}nj;bAWWN zC3HTt+$3eG-qfs=qFx0)MhAF31s$f^8cS>RyC&lca6QWz8+LTe3-h5wn2#)5VxBd$ z()pq$=4&)G)X7Qf>HzMWC4F&Njx9$YUP@cPY+6yWbw04HgD)Q9C%7{r2B*1NK z$1OE`aN!TP&D-q79o~)`$C@kFr!~Pt8@xEyhr=JG+rus|y2|U|$qs|JG4Qql8~Xs~ z9E%L0yfbn$vh88@DijfR-i94G%?}6RAA_!cgs_jWcMJN3Z`>4ilWq>Lq;A*h9mrJBhi=?W z?|cb;st>&mJGj^S%|7($yXmtFkc;t`+Dq*l%0!abDO^k9GX6Fptjg=JlB>?-s#!m; zge2XT$>fVuIh5BVpq%^p94d9B!3Nz{%%9Jfx`hhyE>W|k*<7((87fppw^#bQB@mAg zA&?h>fEQxmPH?kaPl-qcGTOO`{EL18lxiL7bJ6?=+?K}OYE z!w8f+ciKImREnPf%2I18_3|q}IF>0*Ps4P(l}#U9$-muW3H!S>qpvRY`|_eMTuX8) zW#bi^gUnQdMSVGNLkaZC%JS%wg;FJluhMS&6JGx;hDgD7;L${H8D1XQ;fOu9jgMGP zCUV9tMG1l=J(H4^M$S(O1G2j`R*dU@Dw}+ zOaAwE94DGU$KRkEj4(wfgOYfr4V(5NDNb=B%QpP-T$EclrNx*ksw5jYT&K>E7`0TU zD71xS311uP9=@iEiy*)u(M>6g>WWIc8;U0AViZE91+>O`e_E8T0$P!H2wEGoyx+Q7 zK$``>G2@?Z)kYM)|F^Fl8J)NP2SdhypinKzwpVfdxJD&JP&p-cJ~ufhg5)>An`kp< zr}E{|XJ$taRA*)n%v8_fKQYt!16_yd{iu=}20I74YZi*5&s3%nYQF9*Nq4Mu=)2!} fwi}8u_fWUAeg|o}3iLMG=k7@>_!Q;kd$<1p?m3I9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..65b12a42f9b396109e513e41a3a7e7736b68e209 GIT binary patch literal 5825 zcmcIo-EUk+6+d(D_1aFNluuhE##+L{1C>hC@`^HRZwzTdt2Pv))TG*YZ^{aLZT5pG zym&UdaqYMuA@M@$T1Ytm$8}<&rhtURTM|_QT!cg`g;GI)^6~r4+}YhasqF_C``np1 zXU?26=bSnB+&z4%QG8ZZG!PBQgDp5%1hNm1*CHcE)NzcPMq~=(%ZbQU;DNtAI@sTa@9^pG2s`Qr3RKh^Nrd0p0)8zJYAP)pIa(pr=A%Rve<1&VutwesxzO!+*Pdm~ok3*|~lB07j|x(MApbEPpe)1B4oG0y21 z7D4)?3qOy+ z7XS$N43E(U&nAgrEMWK5M1t2b)xexh_au2Q?Tc7+5}+jP{{;G{0M`KLH06jaidGvK ze-5%%jmQQ-HIWxKMAm`Bd3Zq>)0lw>a)ifP8owNOEL}?jgB)z1^#O73SY-DivNOQ= zO8{z=fqLWUUjQ&;6wrSaP=LfXScZ*}A)NubZUt*F%`Y%`QtKF}p_>&1+M zcS!$p;Skn3x{*^l!QR*}`TIfvbT=2pvBm5Az<1@ms=*5(%^rEflqZV%$k)>SBFZrD zIaxZewDxvg%)8vXtAqJ7=fQ+Yq?zD6n)=>bVAOEGuGP|;1w&?lXN%n}IO1mFE_AjU z*BjMJT*{U!#rgT_+Vx+>5UnQt(%QIp)x%)SBo8WmnWm7 z4p6v|d@B_I6O!PxE;vi*+0LeV@JyZS|B%#H0lfymy67O+0!9)euWD!0Hs(tJdsc{P zVJW4p!LPKrKQET-sa?RTCIG24{0MsI-@xQCUI#<~*9^v#BZ|K8$zwt%h%sa1()*aU zM(e$L_I<>&`}Tbd769144|-cU8Thk+z1onm35b+YnY<2uDNXe&lfcIk6o7}%)gpX; z$`~d+HJNB6JDwriMo&=X6cti@@ ze#Bnoe+2qZ0Eotjg@lcA2~7cG%4lPcDZ7p7+yKy&k3eQ2-T?Qi0S}^+JPG)xw99-j zOjn0$JUjQjo&vF4vBd_K?AnmoJ8jebZgvx)wuEp&{;IN)6)aDhQo z8@Z_ybr?8?1rn((gf>9;MMMjB-Eu|=A}ql&sBDNy{bc0L-eMvH3R_wO_Pd#+$))>7 z>{QL?J-ZUNsQ=m?pT93!cbu%wTkL_fj|m91DXM1SJr(20ypaQ?X6pykbjS zm;A#zXh&tRZrAo(toS2J3s#f@?QY|tMcX@?Da193nasLCJ&GR zm_U%~Jn02HwGm9=CdW*s=T zHIQ5_>n!{KsAca%&c6WA!QK}a8P1z42+38Amw`@ofn{i<6{GEE^J|C@=!m>#?7<_S zLtM$)ac&2Hp<1rTVA#Fkh3>zf>}LsWI|?l+YEO2?N2zlV0+otHJsa#Ken0pgp@cnt z*Brpwg8=+nIRz`HdvkV@Ln(XoM^YHO#mjBMc5nRoY&#Ydt{u!f@y zC%6h4-|?XF*NZmx2TiRu1>6$ycLGb-+Y%1exz>qBkJzby6l{-^6-VF;$pDZN;ERr` zimF}(p7q@!tYuPfqNjSqQT;o4moAd@7<&%iC$Z6&0f?!7riD!ai@5FXvpEWOfqP{U z#asdv8H*Z<-bD_(t)ZCy_kvY0!RH7RF$nFmu-#Dd{zATq&q1s3`}R%q0oVJgtq;Q4 L> +-- 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 0000000000000000000000000000000000000000..7ce0e7b105c278834fbca248d3b582338e450674 GIT binary patch literal 300 zcmZXNK?=e!5Ji7V#0r9lP#20v5WGRUaOcLoATfzrXj3e79~brz;$f;Mb83SH9T?`% zpZA&Brj%PS9`X?X74Qj}J?dBha|gC2eRaT&^sNS-V0Z|yO2oL+EfUN%x^gj_)-j6& zT!j}g4huL8i21%M%%w~U13o0TskBtdQswrToPJFu)tShC3nI&PnbJP?dz);SG?&^G d9jShj#>zvJ41970KJxNI?sKb6Cav~nH{KnFF*^VN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..48fd665fcf43a377461175f763559228893441ca GIT binary patch literal 1751 zcma)6&2AJ&5U%QJF9t$DiL{9T)W^gy^+wID|_SAtJO2TF;Eli1m!KJ0rp= zHOTBkL{8uU?=g#vrHBg$j{6XKfk^qPXJ*#{M@nmZyZ*Yn>Z`Bk!tG-4J+WQ3%k<$e zr@w-63A5*jE(-AmlV6hT>eiU-sm3xa!sKd^CquHGqd^NbiZ_dGvL_nvI3Fb`^!C2p zLHKa6jr*3Gfc)_;wD!hfHi)*8AuR0yJ6Tky{?XA4!f+5ifbnc&^y>XAjpCg_8j(Ho zFMFI0HgQA^SMB)(OBa(V@F%ZBzDX684w#GfOZaA0(OsHSS4cJ-(@Vtk;a3c`_<2p+ zah{}G88s08b;Gt-{jjKGoE4)GDYrOZzwsYw;^bq;=dj+!TsCxa73+EDOVEDPy+-a~ z-Ge?6$CJwO0a87rDbGa6GI6KZGpNKd!X^YJC5uQBlyPo2Yxnmq^J zsSV2Wiopb_iC62hZg1Uw=Zl7N%zd&}KBf1|ixzFJJp{k=*9v zJZ?ijSg*kH4JJ;rpJH9dRE7%Hz7XHT+O3RCTtx@4epV58k?llOC$4ppJ+!Jq?C-Gs zfmO5AyxxPz!lc8a*FIMCBgO-+J;(fCgY8?)j-eA5u!~g&Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3be66cf0c45ff5b4f17458f908bea6afe55be451 GIT binary patch literal 592 zcma)3%Syvg5IqxapD2QY8^urPHzZ(Pxp6B(iOIDF(niw+Tu#Lb{(^|#a=Wk_f1yi1 z#F?ZI#f<~!PVT(!Ig_LFGQ0qpwrS(9d8Q-5h9K~PV+QpZm^$D~Vro}D(+L3Z}D^3XaE|0H +-- 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 0000000000000000000000000000000000000000..da77eb8d59a21b581d70b1227aee118bf8136f8a GIT binary patch literal 5080 zcmb_g-ESOM6+idR?5;m58fdf*!9*KKkPwaQw4$_B5i@J=R&iS?lt__+sWsl6#FOmK zF8k3qyyT8Q7F-??5`s{K#2ivdG=4?7|^lRQ+t>yPRtwzJ`%sKU@=g)b5-Cd_luC`OV>)ggJO{MF) zPNP94lji2sdS1(4X|!&)`aLpFCr}OUbsWFja2TaaW?nCLI?nwu$=2!}w}!ph5xtJ* z-yYBXesk6BjCopZwHmJDj}hvv{whoMjHaLP)L-r6&`$uHuHT0Z#*LXuxt{zo#@cSP zibMtTKoKot3>(q_*7-SLMaxtpD%6$18Hf?0#W zpeQmZFBfT6Dq02pH9Hs_z$Y?%YhUCA)`cR_iS73}oRyiP<9@&Ibs**`K)J;Rtb5(I z)2pqqf~FF9ve55&Fq8IAwzInqC1>jB_tv^@w+qXVnM*e&8q<5SWeupiT{sIY_t``E zE1h<2B}3Jen71+AwO+41HrJZ#)S2Dsl*)J9-lc_B>#hgcN-3;+zw3a*D;K-3mE83< zjum3{}x znM`+f+&T_`3qGk8vG7%16g2%ZJ{9!9O8jCQ<46%bgpUj{77@__#v_WvSC@;#tVeOBR_#xxugGFISCe4CgUQ~o_=aO+y5!Gl&%R=!y z_O~E-$P`LUB$t~*FN+9xiqUyA?8G*b$m4>mGpy&u3^wam?)*E|%M?T*`a zI_^pwbUuXoN=m|Mx4}f54qv$Ayz6vp9k1PctyEicI?&0Le(%<$SH@)Tb-W(P>NvTF z+}yf1cm3@vhhU7da;JMZitGJmJ3a9D=W77H%3zZw*xXYA(WGsuNkPFQ%!?${5@1&Y z+PE62jY&HQilI~$q&fxnT!uT&gpn*-q$ZK_Vtl}PErctv+fKr5&tcA25bHBD9`lKG zG1lpeb1;DR53fEJ-R*Q*FtS#6tyPC2v=20qw~n|@?|%D`HF{2b8|WmD)X{G|IksO1 zgV)gEriB@NhUg)7JjB|ZRvVuEWLG)NDYF8E&sVXk< z8zII-s*JcQLn-g1%HZUi$#gVDCh1zs`-DWSMRIC2sNiXNSbJu-)?(G@jryw9UVf|Z z$9LtJtCLg->7UTv2K*=JhM+Tl$G3v6(3g<_XRHv(51xbhKv@Ya1k4huN;b{l0*w`> zFb;Vv)FN;Sh1)D?7iwu}vIg^7XfMR|iGfceGxAh5Ap z8DhgEyNq)Lcm{kX+y@_!U*xE24`nq#kB0Il){5Zw2s|9hMK}sba`MNCu~W&LHgG?A zj?Ckne0I*Ht$q~K3Pzwm!p=AnK?8h8B=>P%e#0iPB7adPgJlb3fgS_nFU$*lQB_&D z*6+dZY^+Df2Y6~b=x1zCTtb+$o@WU7ig6&H8c{VI0Z-8u>@|W-;S9!epK zDzAo|r%WmwY{tG_C|Wr++Jo7S_{cp zXAx9^ae=P$@u#Nsv;_F`hhbK{UiIH~8eYA`RR+q+F(G5@NzKpG){9 zy+1~9du}H~`p38Qg<_?HQ`VC!))+-GXXjJOC<>n1#;hh8Twl+JD{-xvr=(p&IkZ0b+mzlFI)UgV6rLyoOAaOALeT?ym>vVqonBs2v6(Z4~8L99jft>wTJLC_}wNlSh&ZDTriBojo+A=J+j(bJ5T(U z<2X809Qp3OL*_lFsww z*-y|W&*Pgzx2+^FDi{YUsfV^Pj>6=j2&W-o +-- 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 0000000000000000000000000000000000000000..e56cfa114a3ce9d14d1e33e3467d964dcd39c38b GIT binary patch literal 1865 zcmbVNTW{P%6#mAUO)p4=QfawG)jq%jLQ3ET@i=a^iIzeE350|yMT0#_OnmFC-IAxw z(j^rG^;Zz`vb>CC@z!n- zIH2Jx;M4(Ul@c4uiFXML31iuTKGqf!qvIHI*zTc&BFfTTo#*k*T~=3VT4G&{y>)-=DFNZ= zx8XbSZRLxh@?5_@8+(2|z=jXeV`7_Z_+$CqGWu9lY=q=U4`ZwuenmtTM z#@fuHKlFB%rlv`j(VmXt-ZVc}EhFS%tKo|~kX!~VoTcZpgQCb6va!%bIX&k(w~c~!M7}cL1BLmq*vexarI70__RvO!Y4zpTh@kgrP(2Oaq&K_cD`3Mx+;Iv|DbDvQ zC%z+mSb6TZ3V4oMJXf_4Gl7>1qQ|OQT=jt~K28`PI=E+KN1F?tbIx3$Y0thj_shsq z*>!c&lq%J)iRbm%-Jz+wb9Pp7)g{UjH(mPi9#NpVj<qA_`LfPDF8VTdz;;z(8#QK1;GTo{ z*-?mu!xM_CBqOy@(cE?ZyMKHV%c^iND=d4NO{g)H6y%H4_Q!NhasK`C^Am!$X^^Ye zxTP=Nk293-^a##7t$~W~C4?A~^1J6ltvUJfsS_1(L&-S!gY3C04i((5Gt$+c$0xM$ x3+jN3*S^4$w!5*ry_5f%sa-<}4s^P2s8+k%<~p)8?fzKWR=?N+=$wC)zW|nFM*#o; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5681b9f31879c2b1d5f5b916f51fc94e2007a40e GIT binary patch literal 1600 zcmaJ>NpI6Y6n-;xLJJj}Ac#e%SCD9L9FTxXpsk?5LHjfokChcY_$@0*J@_tLaQHJVk0hNZOSAZQ8o7vQka|8< zNyO&)z>5tzd^VFmaJ~7$ru0}()95#YLUf>Tp^dON@DYP+V9H~-VUWh5@0)mjeKXK$ zeP*QHXL=3xrGH|K_K{SXLu_=f8+j2-nngtNf}YtT=j3FYN9?Xb>IG}vB-QC4p{a?I z*$mcU{t?tT1h$B^q(*2)(b^AoL?R>lXs!548GTbEKEayX#dSMnFfXi=h{DJObtgH^ z+VG;T4P8G>45Ivar)>O#0V_PEhZzR`UWqS*ZTJq^)iEEvpQ z1Lu^QgxO(!ZClLlHe@?kU$7WsH(|UaGP%^w!d# zo{Y|}ct75LyWjZ?F5m5sZ!&+s-&th7bIWc!v+WyFw>kcgmsNhlJ!yrl@?-o~9W0)U zYOY(x#eDb%GUc-0rr0I~9E>W8LV0Jl6; lRh1^O+x3EeSO%P~Re{<3MRh_s{3/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 0000000000000000000000000000000000000000..e388c475979d7d0737a2392641b64eac1fd77ad6 GIT binary patch literal 3085 zcmb_ePjB2r6o2FOZrbu6iEwBImJm`?NFo&w5(jE$v!#KkEhIg)5IMV()UDTEymnJg zj1ikC^eg0;4`S11IdJX;<-)h%$nUK^>rIoiJ#@5V@4SEW=Dpv04QIjFvz6F~jU8psKJ4(= z$?#wCztMwss7CwQ#M-XW2jIO4l38auMiP5IjUA2T&xsdFGTA>Ke$w{l3K^|&H;#MH z2o$8Z3Y9{q8+N2W2W>K^N;*i158Km|a_umPqtN$dH0SmDvR(PobKCdbO~`BgzLc&H zXB(xy7j)!9GT+flR$qJ23)cFv8z+4--#x~`Zh>saWWINd)$#gcZpq{qr|^wp-S`4; zj>5O3L~0Y|ib&){>qy1`iOVyhJtXr0NfW7{L$EJEuFxT4Qpd>!^vD>HLAh;=^d7_R z1x3c$Y+-Psj;KxO)Lu>}ozTgS>o6eRCICW5RU#k1R7TAFNW>cO5$X$_`E#YqZFT>Q!VF9cY}l&Z@{+3`Fl>{ zHAq>0GUd+&MB&=;CmXwrDNxXDyxF;iZIh#jkVx@eW>XIC9i_x0+o3(1#2=7(f;m*V z7-qQ;sOUq`-?#sy=}f3yB!Aa#ZjWWuW1xuy%W$#|$fTkD1QbP1YSJNW|AoXJWG2<~ z8rHBy2V&ZuQb%R$g{)mKWF20YvU=Za;naP1v)14Mx9R`7Q-v5g4OFwJYj z^fB|qp+n+doAv(tmE*)+)~w;d!wW0K_QIY7qR$r`vPKBDik@Lvm%pCx$OrR5!ktEg zEs}1hT3tHQh1!1DN0ZW`MJwcr;?av_&y6G_Ge%)m&C2cde6Q`sUKq^Z>xb|M$0AD-z1X{%VSbxz8QAAZqTuUFt#?rB!H5xye$->Tq`!#)#^@k z+Bot8nAJU@>&ET7WS%WHp^G1PKAEu++ya^she6U^ml5fJS507@OdSpA%~)lTj_m7O zrdhOQK9+76N96Pkuvt2y^)BB2>D-T(X3kxy;=h4gp|a9iS#=hcmsYN|7At2LTg$E0 zrR(2RUF|FD;@qW~Ps`&Se|-+p6-OHirHmb0KpyneASX9K=cdebgMJ5Yx9z6bR!xzkrt1!5fd7Gm%f&J2X_gA^ zAxGpCkbpg%9DMr(J^`A~@CT@4_&53*;M@2F@S#`t!>9_;Mu;L{5NJ%4jbEoGlHD*y z9cV=OYyD&$w?zfiO^MDZ#*`$i1{KQ7l3mBOb5V(7Jd!@tpPUxcr~E-xUUdUy0M;^ VPTeh@pMaN&Hiv=dD~vX*(f_8xG1mY9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7ab07b9ccea9e2dab0306724d45f78dbd5caf7ef GIT binary patch literal 93192 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 Y5)n}x0XZ}w_*t$ub5OW1Vff$w14Dr?YybcN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b4421125bcbc650aa6882a0c40d8f49416378629 GIT binary patch literal 1186 zcma)5OKuZE5Pj9|*bbI}4G~H#6p1WALS)Y-(>7$q24x2U%W)=-#n=^yPT?W^-)x(3oTJ(yG>%@n$+m8SGn$MjX2Hgdlu3Om9^V53~D^Q4tE@QAVd zbgFS8|5GxFeCXyf703QjGKOvx?j(f1SJekz9!5vQF-zRwPNu{7v~F+`gkc2T zTJB*Q9wz~lJB1%4qsjB)ko6)q7@SN3e=w-W8vF5Nb`qi(AQ1Nm4sOljTt)!_E71(k{28k0 zcjM{VZZ&;{!g+Pk8nd|{pN8Q_lt{VO4uqUz3J;#qUT|H&lg#5R>GC>P97l63lQEb2 zx8-PJ<>4iigX1T!#(lNP>uh*U>+Fsgl^TJ&qI?$Ye>ajUh1*^MS5HB{DD3~}$=m)C zR?}!8W$dQkmtRFJV@wu`&t)ZDG;l>N*W|IW?G>*6LkboZ^uPfTD+xRblUZE5=g}l4 zM@mrM)zp5!y-KAr?L}R&SDWk5C1q#BsegVt +-- 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 0000000000000000000000000000000000000000..39cb4ec63a9850e805ea0dca835a59967f8d8f26 GIT binary patch literal 32883 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 S`l$ttA(lOY1%cN$bpH>|#i}p> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c67366e63b6b648e9e8b37a58c0d42cab3d27717 GIT binary patch literal 12387 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%0A?=DPG-Y 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..14387da7ab32fdb8a85143ef418d8d79df1197e9 GIT binary patch literal 1476 zcma)6O>fgc5PfSqrBD#!&>Y$d35iNnkX98!aI~eC3tW2br%d9ek(@Xl~! z0V&Ft=5&p(gd4Jd2cn#M&jDtG9tP#qgs z&`|5N`x@A${UHHPpr9OukqtFSQj9bM%D^x5)le%8^l|1J+6GdX7$yczi6JtyO`O`9 ziJ_*gGg}Q4+DT?(2n)BNaf{PQi=p>2ZiODK**pn;TyHH$Uc2kNk+%@i;VZ{Ll5W|`wGk41|WJumXzPBV6!XPS#bx6_lo!kQ_N z3)em@xiFDN-0t%HO1VjMiK-N)H#k0njnw9CCR%NBUbUe=ab8W7{>hlrhuogh&?@sY z{*Cq@LYySPJR`DZ#!0czhx!5aR{WI|m{N{0r*f=f?xYHbj-!s4JKI;roRim_p>MqY z-FNmez_H5K^c`c}F|ra^k4U&ImOxAGfljmjs&*xxBhBNsm9H(Z+21*A z`yXt^C)hlI44YW8_u>qMnQ{Tv6h9(uH>#5l5Aqpy?`=7?O}F-Drx=&E7HfMu7u|PD zGGDTY(Sj@!N)^N<+r19SQ?%d*uY~*s2ZTJZjB!IXSU1L|AGnRZx*NHEz=Q&R!f~OB zNd15S-Xt-1Ui{vtt41ZWOnGE$)TiK;aNOn%ZQ8s&Ky$z@)8ynxj`F>gl&1GmS)q58 zs+3ugSNfiMLKU15zegH4v~9J>`DePnP^FCQee$f!GLx6TAB4O%CS5KM>3&Qlswt-= zrPrvkyRRHYwr$zbnO({6N~GyUdqv4s(CKA+&O^W>8lwDNnkGlt&__FOn&yAYUytGX A;s5{u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..21912d15e3840fed5588d513de11469fca63868e GIT binary patch literal 5391 zcmb`L-EUk+6~NEjz5Yl8w6t_B63a4apLhtkuOK>GuLCBg6|^E%XLc-#Of8ws>w1CPBAR%alKvH?rJkMw~fN zGv_>Rj2x(?VT^s|&-?k*D}*l%II`NnU66|uWzwn41wU5{bMTxEIuuuWdSS#w*p8Gp z^cF~Xf1iNs?nPuu9Uo2_Rh9u25;8qcgi_NJM7`ileB)wSiW(hlaVE8V5$ z(UneX=79mv$>vHYX%Fy?wQhR}84bL)jb`h)0k+;9N1k|@5 z+4w+Cd;05hUwd@^k%uMzNItXh*uygqOwSzs?9rKl_e-aj)Wc#iOW$fVTLUy%UGJP8 zLfh>%TrFI~bwmq;8_m`AmF7v4#8RU(5N>r>RtDHcCt+O$V*}DB za<8@fJTMCo{}EmL4)BVQ<)o4(|C``>;_ATs0nk8d`jy@^Ha3!W$ImH~wl4q=0Tm;o zx{Mw&s#}@rXy3{t`WdKYW--^Ph<#byQh7 zWb^@pGX@s%L)W4aE_eRfq|+*5e(k|H7bqoFnS2@eCNOHGGE4srpo)b${g;3`N_&V* zI44y*%Dgi@=8l0o=vrl9mlG_%nt3_s5(R#7Q^;2B`kXMSMkvPBjn>5#n%wI@dXk+0CTYfc ztonOxQ#Vuf4R%epmc~YohLI#8(U0V4IHS|;q| zZr?Q_KNnacLM^(1>dfuhD#SH_NJKsZ81F~!dHA{jf{`wES;w@+A)95^Fy96`Kw+oO z7~l5v54N(MV&oKyB>os(kr>-LN4tXhsx$5k;}@+puUirYk-igo85(r888W@7Aw@a7 zAu>gE=(e=_PIuO(*VdEPw693R>pG3j?*PnI?xv4{gCy7)t5>Qs-ba${xU)XtC$h}@ zqO9Wc0AP1B~F%Ym%B@y39;EiR#R0rYtz8CDWY9ETyt2 zed1=t`{gqtR-O8zZKM0lZV55)d)Tuea+}{DLfZx+wojiDbznIBTIrP@i{n=z95zi< zHB|1>E?3}X=syOy0xS2@`;be;)fuae9y*@%YFwEP&`c2$J#%WI@*M{%bB9-z@8e9+MQ}ASxEbG&6ICwur(XRlcE05Ewsj!E}#hB0(tJ&~w zpS0c;Gm()-a);6*J$9s4G|tgNHu6WqiPKc6gR_4+U#ZhiIx%|u!GjGKsgAOUmG&0FUVx?s?1HcCsapnJB$&^#Otea z>z00GxgXrR`}WzrJ@7eu%gY7--|{!NU;iiz(UxD+9}k#>yJEVzG40>2pDL0}>`ftz zkK{6YU+qJQP^3uG==e{7dv2lRNTEHa-=h01^M3-2jQLN5I^*J8_bo3L=gc;9BF@KL zM^|9q^GppV=|db04Nxe89>!Xk zdh|QMZ;*S94~O&gDR{A)szXl-?b?H(zy0q!klzD7tQ-07=(?`@BJp1f>Tm|&yKH67 zyktXEDASSU>`noPYvDH51RV$PMW*tK_CGJ^eQfzm|1chJ`a04UzFluPty;jf?e%o~ z7d+f}x+Nq2IceDf)~;J~k!0Mmq2Ox_ix^*YlrUsoCcey5#hj1p4R5Y8)aCrCcC)j; zx7Zl!59s#do$Ary6rGyVajBc}uI(UP9|idH3Szhl1-lC5lXoI;4}f5#GRyh=Vs2bf zB^T{3kh=l?yq!7(ob-+sJbU%`B7C3fAS?9c(F1n7c(+SMT&lT0hz*LuE-#M>7>)pZ z3yrh>@SQ26KE4_FmXq>=@2f73f+f0+ezMQ4RSQSr1zs*0m;Dte6B)Tc-AJM7<=#U- zG_#coQc}_HRZq8rv+s)+e4*_bzJgKvPb2mj6=ex}7dU{^$_A9K8g=z0<~2nR!K<}3 zrC0ZKPThaK7EW@OFJs@iG=rkg0sLcROpEvq@CqwFtcKJd9C^b^C?D+0RZIMM+-8Vq zVpye9UVhJf)&6_aJDBmX#SurIEwXpj?1Rl(pN|Xg@CSqM=i)Xj9!w(`*B1UqRbc)< JJNw$z{{SHardware 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 0000000000000000000000000000000000000000..59f64ba7ab89027ac81393045f4637784ce0eb98 GIT binary patch literal 637 zcmZ`$%SyvQ6g?BCFN-b}w3Mve`U4`3Rp~+nX~AU~Gh-V}CLu{J-HnU7@EiP?bm3q4 zG2S#uD~bmu_uexn_cg~iGjR{bo4kp?u<9p7)_Qs37^`6zYIIuJ#d2+X{kPn}>O8#2dp} zy{q@UMwe1|VQ@e`x9F220Oy5=TM>cR%V0X!A_dDKZUU!D>-vLJ+a+rX8WO#qgxes``OSDKaEgBe6Oi!YSj=C`kQ6SSS(C@I9pI1gy*zXpg zS)hK=2U#D@2okR%qJyYF`xr;vTcTeS6FcCAgQyR9A00-2|0utYs4V);elk1#v=1L~ z+)uCAN4H4(7>7hJweA3P2bAu>NnK;tC-)R-!>G$*X&YXbf zSostq+Ghj|Bjt4jyh0{<&F3;6Nn;Ubv>oSCa`jHWWsc7%ZgM^hU>M2T;w)Oq_$>xnp|3V#YYfSkoL$*X)eyxCt0WvSvIAJILgIfgN`5zwD_Ib4=Nf5ePi@tiI&8i(!%>Rt7Y%(*#1oz4V$V?x$u-vLcPZvuLEUd!*aMkQzGQ=jp9 z?b85;k#aT(Uz3Upg>`_%$1rgLdKWUiLwfYGXPx*$x{kQ0*NK2(q;+u-u|Ss8g~DQB z;!=hk8jpK@b}p&dp~L-e0K-UQpMw3Aib-K6FpOj!*6RSnVC;~->E%*Pr(BxTDVN)|keZW!>5k(}lD&d$mhxID|n$2yl0FpNXv+aw?V7=-$zKe9THp&Lcw z(D?38iTy?-KB0(S;GW5@G2edHc6^(PXMxI3qg{1on_kUs%;JfPLn)l%!CpW`dAG@r z)TH7-I-$gbibXX|WFM(Xwe1I?##m-?y7yh?olOTl>9hwWxSX z;h}H2LBo=&rxmokR@$(!o)hkAdj!qbHr5Fp&!yssGI+4pvTHC&QTA-Pp1ard+5v9p zEDTRa=(SRP-u)x1(MA-b%4x%^T8$+HwrlO(vzm~ME6J*ql$5BH$~d-~y=k=@q2AxB z6NJh$>)5XKv|-n@&hi?9FFjhjr-R#dYFjqkaB#j=a}YaIEK^;ZbD&0j3>auSI z+j_FLv7-C6<=d5@ZuxetvTdX0xsTU;`|}hp!K2R9bKStQWpyE$?a({6DyY({f(Bh0 z*rV(^uCVpqX%Jd|uKqUCDW}@ma?$^~K$g{|}oK#vIC=BY}Q){AIfAL586{@S%9+dfvec zeI&H%R$y1=D}fz`$S=z2oT`Vazt;-AO3U|JULgCAPmwigQkuts#wOYEBuldC?I1vo z^%)hUvTi|gQkEhI2Mw{Z0sa_tKC$@}_VCo9eGLN33(X6ci-$NnRVrT`Cp$p1UphFXh`m{jIn5R@lK7=?I!Ql9YfFo6n>yc z40mGc!~(yY3iMeTkC-u|LO<3VFmDzHIyVbt#1!H8aU2)uVcaRqbVz)S_ti;5L@VMi zydwyDVn$-c)Xy?(NmN#tAO2&A`|;Mz<;vQ6d2#u%KC-G$o!V@| +-- 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 0000000000000000000000000000000000000000..43f9825df9d65c40d2adedd32a5366cf4deeeeef GIT binary patch literal 10156 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 W1#cKncslU8z-cA;5|=Zj=6?bCu1`Jy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6f6a81f439a49c6268969ea1a08ade595fd13cc8 GIT binary patch literal 1919 zcmah~&2QsG6n`_e)9zNg6lvvFB`gsqE|dj{OD~L*CN$d86|@Lx6@h}|O|4Ds;E$z6 zNSTB-95{2LNB$5eum%1Fgg9|QDN=YkfpAcEZ=t3}>N{dy6WJbck(^2ik#p+Uk$d8huH_s*B)yW;_Q1CvlCBV{b>jEp0aR~f zTN=c+yB+r3ZM*N%BHLlte%S3ptuPB6r#l#w3W4)c>;_O+TlO=EyjNcQRCpJ*5KCgi3hwP#x*!r(MIwZK4Dh}Hr~>$dW9|~6-qd1mlI;MIT}clnx(DK zyYNZqEFV6(V2suRmy)}_S&dkE5!{jnDC3@hhxm5&& zQzw9rVy@VJ;A7HwODZ;+Ai};FQOg6R7=J3qf=@c`RJNAFbfXw~79e`6uNKn431c;P zaaGba3|tQtdNJP~M#^}n_WGSWwYC>If!7_>4xQkU6V#eP-~;|vuG;V+_oA1N&W6rT zEpkqySB8VG>s^0JtA8B0Dy?Gt7g0)1 +-- 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 0000000000000000000000000000000000000000..624406e5c8c5b0965be7323a4bc540e638097258 GIT binary patch literal 2401 zcma(TO>Y}T^v&+9?S`gQfSdxMGEzAp)DJ3g>4ja}>&8iAr|S)ZQ?gpFw;N-Dt_<-&yzaY1lGaNx~)$K#|IMq16h&w207``Fj73>sH~ z&RAd*L)oX?UgJ5R4P4Iw`T}D4 z>bQ5@?zw^Qwt>z~P|zMU-E$kh-vfGV7S+Go2YPZA({lU0hTq(9JgG63Zu^~gF9Llr zVcuwTT5YG!Z+H4W8Y~yTw8=pN3*X{q>HsAc!F9&qGQ!J@LKx{#W^5FKfjb1D&UY4s zEQUA3D26vf$iudtCuF36v7^9a1`6`u<9OV|P%m(~Fl2m(2VoS$4?-w%cq`QT&bC4p z!w5h^HQb+^BN<K!7@Sk-j(H*FXh&fcOvLnU;l9EXL=b#duw1>Op=)e$1n7k-{Ps zRC4b)929`W=O^;4(Rt7*q!uaOUqF8SW%d=`m4&~D3;Jp*8_`65ekwnaVN;oj&ZBfl z3!O2Yo`~hM>@U84Aw~)eGVDv!6w~17SHKv-{JTMK8UDtpt znwW5iGj>k^o{6Nh!*p#z|7nx^^Uv_`{l51cU2(w>?``>(@GO^N1#UC<^LK@?yWZ~x_FJ}z7EKBE()AZe2SkEe=`RZbc zqLcAzw!6(wWf2yt72B-X^($uOvb_Xo*JKI2eQnK@+?IKD-K^R5RnuOo7J(j5xCMUQ zD$99hL{@?~sIsxGPJ7<7SQW`PS&XP!$%4ldT zEm}^^v_3Gc`kGa>tA*+n=>b}5U8~n@t6aGZm>6N_AU>nu>rta#NW7Y_h}qP6?o&fwM!loMh=(}~c44}q3^ zzP{6$fq0{rw8^{>rddg+pPH5o1d#}Rw}Y7}pikWPObWYQX+hsd578wtnwnj&i^Hme z?83;V=QMnT#YCH(Ng~B#KayD`CHsEEA7J=|87+*bq+V@&1$~Zj&#VlMjnJ#AE5nA)`;9{ZdX)PYUWQ;&4izzDTp%yZgD!iugxBdXIs} zKp&daZ=-J)@xzK$c}emzK80N^i(^OA0a8o{Bf=k(2*|4(9|60EJYS;77VaP8&aFL- bJMUy%UbrDIKALl$)4VO%zSdFXf35!j0hpx- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..980e2628ef03ded5847e70c02994319d4778bc5b GIT binary patch literal 181 zcmb34DNPJyU}WK7;b6c7Okhz~poF6X1A|Nh1EXsL14H8h28Lq{jtnsl4UBRO4GbW8 zxb~vd!qUv5R0b9O}OQ%ZAyO1PnXu#)1Ee2_LCs8~rx RQEFmJPJVJ?4pam|0RSc`8Ib@0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..51176d875b6471aed4fd9e044205a4ea4153f268 GIT binary patch literal 14779 zcmcIr`E%RHb${OrAV?{7Y6VhgCc`vhEkz?yP8_#!5_>@ckR`=OETx=Elr$g!iWorv zjl-0thZe-)NKRWPaneSnlO|23)8G0ZRbP05|R#z&l6Lxe z`~5Bs@sO(|D=ofzynWZceS7kO&y*9NCBq)FhiGFk!|Nz?5F>(UOp*~px*~~Yk**`% zbct4wZX<0N2CXCQ7=Vu$G*$sT(v^xqvq)b@S(@~^rD(&H^A>ZOWaJGVU z%qBVlJ*I3_lVq46<3QvB1j#%AN}`dp>wtYbs$hq%wrVe_d`;)=FRFY?=WAZxW^@wp z_Lsc&oL61M<&2jLEa3C%YpPz+M4K;rI28`&g%bu~zkCf4FWAFWM||5x2DniEvdTv} z&#)4B?XRe^S{KI4w{+hAD%W>vx48XRzm0xsBfmGswMkII|0UnNETqOD?HJi)JEJJ<2nkW{1p#B%3C{W zE&9|7fA`i^y@~o)RlcqB^;h3UzayAVdncu0oR*F;zI4qP(i-77iDWvD}`Mx}8W%K8IJ(IeaE+VV>Dd#fJmD8D24?%gclwk@O5-Q{v zyY7&SiR8J3Os)j}NBsKC;`0x0QufH5Wz4%S)98}ol7wf@v)ydhUE7U+yy;l$4A8W>U^Xk$mg;!;mXoJ|yqOVFv7A-RxFUeJYFcrc-hG~Y{^d5i@O07RLkLBTS) z3MSK0O!57myJuO^1i)wa7 zJo$7jokvEwlva}l$`=KGHVxw_eGt(y$YyFxK_c>^VK*p+g(*)Wzeh+JmWGU?HZEPN zxP)532?^?Cgo*3Oh*J}70VA4S&H{kzj`?D#kjb6bjG;nz;8Gz|O3!C_{Ewmmhe@{F zNes%#3+X~Got^9rX~xN+?2T}3y*W5ZRpQld{N0bDhY&4`hFI?Ff+#F)c?#*UOR|M; zhg{>fD(AeUj&O~RbPb4TL=DImam~2VG4 z3|~WeBOB1HEYM)mt>`xbcM9_1eI8slPqF`et#lIQ0gKydIJp*HR% zjD_)o&pmx`e4@|w6c^Ij>~rNjtYU+ibY~jf6fL>&R^el)_&6e%H&yQ<9UwswLG%F> z!y=Rfo7$w!7R|x)zT=Ng!i`Pl%GvDrM<4rj?wrlTZ=m=Sh)h(e25BZ55FjiE3G)Ql z9e11QO8Y0c`$9UA0{G_PlPLZ);xB}#Hd=35*_j3}??xb02c>=TI-R@%K22zKa+#Y4o4)oqdC z{1h}17LL5lUl*Q0!$lRG#c;Q6)Ja3SHu%8+2Dm95Y29#W$9j~kFc9_ERUL_SwzRnJ zhtu5l$5(Dur<_R3q>63P+chP(0kqJDMD&*b$b*Zn%x1Su6m43njR}4E?FitKBQ&Af z;j3_Io%Pz+&>nib6F+ok*|xL-t?8jLThcnF6xM0LX@zD)bT}+SDji2Y9;-X2Vvq$y zjzI@&Q_gUp-U{t@0VgcLwL_yQ4?{Ja-sRBnZV>|xL*WQGbtjJU4){01F7qNiUI^(}Cp5}@ zOSEqzt#~vf`Y|YwZq(bM4qyVEPADFDEi@KGpD|wXQy(t{JiMmd<;cK}dTe+P5qKvV z2OXso@D>LSt)DpP9G!NCJv=|V@+R2W<5vHW^#opxPW825=f-QW*&&KVhv-_1=xWsZ z;#GU?i&txFUxIy`r>A8wEJ)r&ucBG7*KRFHw$BqP$ieIe85*G90o_0xG0+H%Vw5YL zLatsOI#4o7)6?Bgukc{s2LtNP_Q`Ugn8&&cbXDN}ho10r>CE|sxjb(;v@1v?y@je? z3aVvVy0nn)-<4!Dx&Bq!#Y8E&&|5Q{-FG^1a6WP{e)!-g9zA&G!2XHifqfGv4(#vw zLbbqDD`dTFP$ikghkLGXJ`az{c+smWq!yBWi?>v+crj6YuE%+v>#;V};Ji}lE&F+K zI-f|TSPTu}EnKu>vRII7EzBn_X0n%idEO-PzCyQTM0x@Bknl0Q^LTfuP+nZ*<+=uk zx=I?HL>G#SAJOGQ8qIw=flky22ItT6Tp3dE#KZwemhy!QhYAI#q{nhLpTEF+h=2!0 zzU+Ep;Z0M%tho~T9#`4SMJ(DqnZzH7?o@Q4=xs(dPmK3j#M6kTVFe$U(p8=~in3-z~L*I1Wi1dIrcfEVb@n8r3`R$vArTW-LC{Q!IO z&2V6%z*K1Ahh<{}vTgiuP-FIvQR2&cpXd%Be}~aE-xM}lT0eV)LbSJIAqYsVSKv+xWkI+xNxZf!Dk_~r<@SVrP>Usau`!s2IX-mqlweiS)*dS4#7T; zO~Vsd;71K<#v@HLUTK;&xX&c$n@R)UwvTd=K}E5qJGwVePv+ ztuvD6C?g4W* ztwPJHXY87R<7~j9H!QC=q&Xv|{N9iodc*HgAKxGj%OpDrJT-HB9I8jfzpEbQd2_oS z)pq~?>tS}B9L|1t9J6G9@9Oly$I^t`W`rq z(w>`-z{tM4X}k=kUO|L+7{MAznFeoz;xG$rd1zJ*I5kkvrLNwQD-QxkZI1Gwk~o!s z2S?ZO+T9GS3gWJ7Ohx(zjJIwHfQGG^^rk$m$&A0o81O^B<56d<8%mYK zy|p$O_m6~7_T#x39*F{@-jt^VkA9NZ`720jd-XNT zc1jqTN^00^cQo3+3N>;#eE8A9|6cJXzdOLjE{~M}uQL=YuI;fRa3m`>VFfJm()|?R z*%^f*QeM~T3RM)lT;7zGDBzC5MA>23fZ2w{uHW}2CkN0Rkb9&zC%kJ{{(caC8WFmF zKmtt^u?8lvurZnfF5Ja34`r-ht>+ui>|5v-{*D5M>sV0!nh{=`OZLK88%wOO_sHYR z!fvht*HpzgUKN2RtZ4RzG_Fs3ijfutPQfXK#rx!S{<^#5ZI$08Z&gJw0`064YeZv`rP}~7V)B;ScZrjojwhUBo zv@lka#+qfnVA*o$MLQZaVm7VV6^zi3oN=2t;UT$Otl|{LkeygPXa~?XKqHk7+BT%| zhSL~XyHQ7dK+edP2k+twrWk%VwMW9_$d*H9C)OV6)Y~{AqmlJG8MoSP_4V4r7gEQk zhc6sMue~AHs-OXRuA{$M7S4aO#yb12E2G>$E@nHM$hBo~B}!P{lYH4G^9!|jDAcq< z3G_>t{?UU)-4y!hlqw^MzB13Ezu5R504|%C7aaU`wq96!R~%S1P3Gp$_1*AkaDU}7 z;WehS`(Tr&AAaP_{&AdY@UrXxugP?J-=PEh_x3jDXMGUASTD=Yo;>^~O@HEYy6~~SLjcZWtq1k0;GKBr z=H=XlTz;wVw0ZDQfcExwbmx67XaV+3EFIYYY5!CQ9i&3BfGba(Nny)3xI57`xioGc z^}1;}6eaYY9bNNlfXE9DsBVu@k-VX>~sO#lf3;o}L$6UvEg>WK@ zRZjpkv}S3>rK?Z(R8iAl(0gyY<*L*t^Eue0nesbobIji zmd8tmaaZQgfY2f${KtJPVGC)NWWAy;0C5~0lFSTtV@R9wRz*-`74^*uCRqxAX9sdQ zE@oc|o&=7N!xZq^P_x%&3JzzxHm>Qlk!VAj&h~A-hXKdCtQNiNpJZBmF??J+wc|mDbckG-x#$yoIQN_ z@IxH;OE|mwfyW3wsjOIWWSHy<3D`1p9>jpw}Kw;$3h8H{p}sVh5wg z2%J(ffk6SAP~lPkyYW+knJgh<&b97C^)cyfQ5r%RD^k)?iDL_tH{5Q%qUX9-1iF$) zHb~Of`)%Cz>EK~QuWw-RDz7Wg-%-i=t-@t+_{WHl=l*>F;?X({sx^FhhGuS?Qk@_g zYIX)!?8RMhWnq63mX5sD6jlVsW$Te;zDt7D;C@EmBf-i>S|Z5n51t9tOewHzfd)e@ zqZ+c8tD&e_3)Plup_W+>+0}X|YBfT&Y9mCo;12~xy8KCZr1So)>q5PKsrFBN?TaT6 z(3=XAxV)Rh>FnjX{H1ZcH5flQmwTw^&3wCk)`vbj{t-PrcoPZy>7j!YCwOK#{p^Y8 zsgpBjPJimDnQ{Id*Avdg$NiaSVqzeHiKlls!L%nl;dO?RvCXTbO8i0riZR6(sn;A4*yPCO(T>K%q4d0Zkd zyt#nKahGSl9be5{Gv-6`jLQy6JP(#1BG9aUg@DzGvojlOzJ6Mgm7!TN5y8@e1L*p%i9=+{h6 zQ(szIedT>=X?x|&)3nnvxmPgt;frzt9-}ce?KGDyVmLlox(5DlF5_-t`Nj)trulgr z-a%m68JotI%S&6F_D-i9xeIS*cT7hE$1wjMwT!DZOj9o4R^Ux`Q%v33!J%MnfTP}N z!b$*sv|7D-bb9p~V5xB<7HnDpC^bNlT7cV`N2}J0SEsDNb@V5Ds{tBSO#?Cv(7^ik zP8!v_(_q|tFq?+yy;Akljd(zytWw2V9{Z6|@XYHP#jABB*qA;NoRQT4_g&cCDFGII z6YxgV2LBfQz}g`@HQNc)UkmL4nlO#1TiucNk>Ls+YmT*n(_ve2sIr2uVHFDLdk1(E^Bh5zr zi4nASC_tf=82XsoGfim#S3A@KOsD<&NFe@tC=v+KdL8?nRcEAu+vOKyM@N|M-%tO? zm9YCSS8fZhozhM9DC@=^!_D&Aa`gp)%WYv&;4Rn_&oB>CpQm5U^uMmeb>sy?cYDDO z_=)6#desJ-Qvpxez@`e`YayQ7ICNjw^d|kKzC+s-U)2RYP%4&Ed^OjfPZw~VsA-~} z--u{J-qWLAu6uh&?|ioll*F^Fo>O0h|MO+Rt)-S~9`n0SC+Dd9a%H}pOO`VEK9b%( zLi3w`K0WZnaxtIHo}14m&SPhy@iD`;FP{=dXBJp;%{;g4$%%CjU*T%8V@IDliD^pj zUJ9D5ZmeDr`Sku$bg9ji=X5haxoVdTGV}UWG0-&sle=EhRD~*4kD9*9kO& z$Om#C0#Fo@XE)`=g?EUoXFHxnP6=2gMv8(HNC2M8)w3dG8`7og3`0n}NTZ&2zi)!X z9%`zyHa@8!L(*ILa-EAhoo8!i#dPJtg00wh9gHkL0@b=(TuSvBp#RR(*X7pRuALD0 zeiquKAU|EKBQ}IEzzD6lcxZz!9~sO@p-#+gp5Aaf2Yo2k$12j8k+?;JkY)r6LX>|H zB4CzMUlzg!jxL+tLsmVeSO)LqYER5$;qoFjo?C?Xf!PNTO_PRr|6fDo73WRD+yLT% ziZC^UV->h`l*KEq8E--gq-Rl9!?#fpRs${@rqhe#4}EgnFWEo-yW^f%`~>jMr+Gb9sF&yZRde6dLJ$AO z-*L!&m^%ww#t)!6idaEpt7{0CuP*3C&xUY!k8>>4sq-3FcpWK7c94DxC~o1>LqPU* zz^P6qy!L6j-kb_JPIE^NNBFE$E_0eYGXR`6Uqx`}dsFYfVd!3Z+cU#QppQ?nPH^kI3oNl^v0iwj)&*=sVkhDfeZ|FfF*L{X z&q)A(25(Pm_%>o+fdkqDG+GRaG4G3Y%I#hBK{>!Nhcr*HukOrK^XfuZZu7|#e-O*T`4-@Z!1c1#UfH0jN`eivx_MAa7I zvuJkWUa19rq#m`q6Z;$p&D<+7?Vq z(oXIOb{F;C{359jZ-@0!+P6tRPGL%mtM<|Aopw9vckGRua|t(|qbYgVI-sqfKshiv zppDN_leVJqdYbLaXi#*ng%DbyqqZK6|e&N#vU-3V_uQNSU u<`)1?Vne(9aO(_aj|qo0UFzAWL-kim9ygn(jTTIG-tc2b8plrkzwik-nq(6I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5b9c7560714607b376cfc1bc133c17ecefc30a7c GIT binary patch literal 21092 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e261e6d5900f0e913f2d52187a0ed5f693bdc7ec GIT binary patch literal 1055 zcmZXS%Wl(95QeA4&<;-!sR-o>@(d1zu&IO)sC$z-#H4m2Igv$H!<};L1#?fKi!Rx* zW#Lpx)iyz;3#1BFDy4$wVBEwrN8(6h`+WZAKQrf?shO5@6~?SdYZCq+pP8ct!pRrU(u?Xb|U2d)=X z>V;;zUa5p`Q**AAO4lx32g@4e-1M7pYV@ttbuyak*`h78(NLTJtTbY+N+dD=RV8j( zLAvGX2%XSKsPbg!86PELQ=QA``e7oOPA$#Me4Lty@&}rlx{rz+D0PL#drHefb3Y8q zzEX)VlFq(Ld{fe3iRc^VpFoqhG&zvE +-- 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 0000000000000000000000000000000000000000..eeeacbdb7272e8a808683c132f69193a740f74ad GIT binary patch literal 2261 zcmcgtPjlN;5Z~34ohG3$Fw-1J05c3zxTNR$%kM#_Ji=-XFV&WJ#b=Yp7h>oci;Z`?P~q&LGXasz2V-V*U5SB zD=)ib7(Dbpo8RN8W=K6L2h!l6q?QK_n^ zK2mftQ#4gBomYv+)j#P)m1vDfKUGBc^yo+7jYqX5!=I2I&uVl&($Fc<^O;&Y9l7{k zH@wqoiT;2d$UU854_juh%8=R4Wy*9?Ez?AiT5*Z)*Kkkm%b))qX+E2cev)r5M@Em~ zM)_=}elfh++!lDyBDqCU0n%kG(S7zIY5EA9Mf#HPhe4EtA!Y{MaIN+kCq;nNs?ZhfWQl zZD$t_f^_zn8D`a~@6fWxdJTd+B{DKoBg6le*=CP+eYs2N8ti7Q=qmGww-ot9McN@} zdZB2&s#$zx47=y7)Sgc`vo^kI(%ZgAW7xWvWu~Iy-xL(}nnz~m{){1R_hI>L~q zvngjAK*>22pzn+)9*fTTg!lz`YZFJitR0`3#=cKeK#(t(Y7VJix8{(@0L8r_@s2-q zHUJB0Xep&Kcc5Jtg|*2m3Mtd16&@9gX19j{hY9yVu3h9mJPMnGG<+qSuL8);bxc9l z^MhYW25}m7!g*RN-DY!;Bw_1rr9i@Gan$}?ro@5-*LzqB|NeLIMsmzV1LVypG0D-a gj}h#cJRYNE49_!#FlS6RlMJ!D7_B*!!TL}14|5iSGynhq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9aca38e344a4cd01de35b7018db57b836a7cef04 GIT binary patch literal 15174 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 AS^xk5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2047960288eedb7a01e8fcc9d64cbb45d8ff0fac GIT binary patch literal 501 zcmbVIK}rKb5UgG%7`+4$^`PVldBX;x5b)wz5r$+43%kiWI}091qQCGlb6N5Uk9ml- zIva)LWJA-{)jeI^b9Gnidx+|*KK3o?U6Sn4$0=~_AveUY9(W{v4?qKfj0K-5WXoDK z*?7S=?NZ1l2pV$JfOSJW4Sqd@N2|J%w&uCctbsa@5A%AKjhrdUTst!=wXKYWI*QfG z*=)IhI*z$pbxhY$tmbvjd0of%yq6;Xol(x_+G(hhKQl|G7ZsAov|&I$r6=;!`9OS; rik~pvBH)de1mX`s_%nE7Y_Ah?yLF=!se^V#w!Qmb8vE-X)QIF4qR2)H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c16ac774c3ec69be09a5fd2836d20be01dc7498a GIT binary patch literal 2329 zcmcIlSx*!}5U%bWR@R8e%c39z4F*X(!1G1o3_(!igGUH29$A>dY*=QNVK#_QJ;MU( zZ{U$1GE2}ze}eJH_|+UcOP0$7i&S@YRZmrQ)mKaJi(2Uw39VD>q;`^Lrl9NqKhcSN zMf6sYSclvQiMAkrhb)AkKalzFxgh!m{sk@=eo*kmTA|^u4N8j09Oq5(BA*$uZU!tW zUT_ZT<_?RB7x|U#5{{Z@*vF%@*bXxNPhYNY1UotG^>`1~w|83YL0w7GLaF3dK%(Hw zjrdHt_#7l>m7L|~w&a8rm}J2ZHn+!H&+?YMLsn6J74^Uu?Q4sxSJP-Gz>v?$Q!oD?C9MW$g&px3EK=OdPDun&Rpc`nKwavl}S>pD%~J(DUVKP2kI zOZ#EtH*&sBfX$aHo`pnH{LQV-F{;ux%`*5sF6W~b-X;KfVj))sd z7mbjpn+z&)(b1(IBk80s=p8-7IoYSvtI)Zx|W8eZ5$fP09Uc>?7$Os5Iu%r9L$*O z!rr4Nc!C_|0_-vRBm9NB2%+a+lvb>9K1mX9^d^3e7>O0P%C7dMNob#ffuHE#0teV{ z9C;Y}aP414G7n!&OI6OGnRcvfUtdD?8qD=!z(+Nq_L + * + * 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 0000000000000000000000000000000000000000..e7211857a1b7d08b148b64193041699e7f1c7cc6 GIT binary patch literal 68124 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 + +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 0000000000000000000000000000000000000000..e64a3381f977db0bdc4c9de2ff9eaca3753e7830 GIT binary patch literal 3706 zcmcgu&2Jk;6o2!vj#I<|P`98$FMJ#k0{(#6*iH{cMMyvqGO0|ww5I+Dc0$`z-v&25 z^vop+6+*os#J{kP(n#elPcWZM=M&789AY7;i^@TLw+_-FvlG^AM1y>3v+#MV(1}cd-^?P z=^^CHp3v?^mM&m?5tPE*Vf@WjXBd;MfGTaX^<{Tv+3(!y49E%w(l&2zU~?&7P-mmn z8IH(4KV6UF&0f6Onu>UCO3g&Py|vltmM43I_Lf|;Jje4DWO>RkZneMa2+x@*&&^?H zGtNn#sg1kV2OGCb1?N}2!M8K&_HML0!(nNv@XjoS8A~4s!#F2+GenEC)2`ul;j5!F z`tJ^qDyB;0^lzlN=~xNW0)8Rz>eWua9p55rK$Qk_Pirq@<8^encKH>IWAq(zDivBp zw1)_*Db~A=C`^gJQ)E)b3ODKp_`jbx{4?z-1dSpK76Xd8KXGagG0HeQ%P-1W$Sxc` z*ogaNMSp2?u(aB(E?nsYX1zNY#W~x8|F=MU2VD#iqfQv&67`IXxLF2Q7D`WH#km)r-cb)el01!rLd7At` z7uLZq4%V8^v@nj*B^GyF9*=uYo$A7!#Hn+sGa*~{KOmEDEB5F6P!B=TxB(ZbuzBDv&q@FL@vA~x61C56SLIgffY zhH<|&3)cFeKZ1un1dY$?jZaIj=85gkZuK)aLG;2l1oaNO;PD>D<9MiqXpdPkScNc9 zvhM-dLtf^Bmm-zoEG#OqLxyG;!2Z=_l4IurmM^0s3zTFH|19C0%TYl-ue?SYoP;d;L z{Tkx|x;!a|4yXW+2n63$Tl`Z^=H3hWvy$zO+jKYJ+Roq|2{Yz4TPdj^L zGF=Pa$Ef9*t_Hf+r@GoXU8~S_>8}-FLjT;!KPXILzvzs+pW1d{lRd`Hu=x}7g?Z}Z z2{23iD z`tq9?Me3<_FkMgw*&;8Q?xGf^a;=tH2ptuhbhZ{^L->lE&t7(^hl#{^!Ruu8Eb3wY tO%H_^w_ia16&;`V*;jiV^D&T}#pVlS;2P+HxTM!yjS&|~d%``!@*e>qIg0=Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5c91bef59acfb5b2da3a85a79a3e5379b946ee00 GIT binary patch literal 5034 zcmb_fU2I%O6+ZK`>m-f~g0i+86J81wR3cPWyk_oQ$0&_yfks#)A;@;t=~8EH_J=lo z@!4IskS1yRlfHyByi_3iT*T|$UB@a)9?MIyDplHN)Pew&h?ei%d*|+^v58PK(%hLj zbLPxB-#K&U&c4~k{0~UkU3M4US@f5l1TlviIHD;gbqM_?6P-Z+xrpdGDar+k4mA}$ z*HLs`X;Pu4t$@^KvrC(uu2I_1OxUC6TRqzB_9$;QXsXRLQ=HC^^G3eFif4M$`6K3f zzEjMRTF=pmHtD(OdcHu!VUZ{_Om7&XUz$9-rOkFsyVP%cUe=($)bAx@@W0eITGqeZFG|~f zx!=}U&UsXk16|B3S3oK_!~Gt3y6so`#-KH?_KVWCU+uT!@#RhPXb$7c{a!qNZL=MZ zUyBTBvNW<~lq!(oKXvWy!>F-bY;+F&_hD~=K3?08eIJsIz5A-ApEt_Y5?QDem-_s| z^3bqSex|%ab}XGeU8Dk(4@LtCgztfsEDpa?$PgP6kw8snx6H z%4xEG`I(hd4cT)v<%st@(5RP}2O<|&DmCojSr!^gOQmY9{A`J09}#`v?A~;}X!z6G z%HmTctoZyevA$f(CiV&Ms*Q7c|;eMfu<*Vx7Ny)rv^@5TvT~Fe@ZF89PAnQ8-|nBVCAygMg^5*!cNJ6+DFb z1<1UN_89gkIN}LMra-&+hH@0s!1ldXla4{IIC~noc1z*%QEVfmK}?%i57OL&d^E~w z((9V(`L4@Fn)Ov&QSU)#(L;g)I$;lDxrg3?t~XUjGO#*|dLhu}uxgnC|8q^|CL+}m z8#kcuW+xK)daFt5UDzWsHdP0^u_cQDw_I}RvZ2HylX;T^;0Wi=3Uf|imi zoq#SI#wkhARg6p0MDQ|@(!zHQ=*%{8)ace4qmN6|db%J0M-B2voVL7unAoBG4D|D; zh`OBISjSxIb@ZF49gH7HaKx9D#Xe|Jkse@Ew(p%-XB4njP$g^R6z%yw+BkbS%i^L@ z<@7qBxez!r)#Ml$&=nw~DL8(dot8k|7bL2w4$&^mDW%XRNgz@CzC>OS=qBP-0_bKC znJK{Q04o#g2$uRF{}s%T1X74@t%)A@1L$eMExOD$n|i`2xN1aX>d{UhIx3NQp`*+O z_z|q;BKlJ}9Fu|Qru>0`yU5@Gz*Q3Pq{pda#0EJqphFJP4P?q`gurYV*)7;DZvHb4 z!rPeZz_z!sSAqVvW-t?AydV1)=)?GjL~g6>a=h*C%p%7{i@=F%11U4nMe-nNL=-S&g}_Bb z8NMF~$I%6uGA{uwcbWx#652}olKxTDu|R1bZKA)RnIihZ!3@zj?_@xO<*w!T;P``Z zA!2f9-jF`zOS!fKR&QeNEmS$QUqL(RL|4PHt*~nyv@56u*61TGlT*o<;xUqzy6TcE z^}I{^1zt->|epG8uHtMHVo~{hi&|>@> zlOd9EW`9*!e%JCxaCC*s(AWcLZXoFQSq7?`wiNT^bxuinSC;ZPV_|{4INFjNKJxPpfB)f z%Mo6Ips|u{GNU{8n9T7Q9|=2j0=&t1FOPFz3$zzkdcp@=2)59^BH|wQSwp~00X@#a z<2eFu{v6zt-w)^`d>4^>W4cp3N65|*aWePswQ+K)z#px}84-k*6k(5%Wbbar z&gib4b1kgR4QOFXW6Q_kFWJk9HozHr!f&hubBXKNhE_MVRq>H-H?cJo+X?#>VJmSL zXV=2cd@HmGW)>JCUNQ^~$9{&)(QcT~MFxx?ZABTjb6y@|Lu9@i$Id8w*$y~D1pOsU zu-wq3reZ7!Sh6L=<-NhurdZl!?`~l8I<#{nSRz2hR-uI{4e;$tu_R#WfFCF4K*_Ju zXozf@#DV%U`1ZpNiD`LAWdWPr5!w4mQk9e%Ubcf9 + * + * 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 0000000000000000000000000000000000000000..fdef5f4c8d94c2b19985e27329436d02d16a4c70 GIT binary patch literal 2733 zcmai0&2JM&6n`^2YwQpxRKjU#ih$PTBWh1Q_0rjx&_lWOgam}h#12|1vEA5|_R>6W z;z&sS3y@U_sei~i)Ry4P30X*0udP(Ihn_0+_jY&eIE3($*_nCYZ|1!>{@})g$~UA; z-sI_Z(0T?)8C8seC9YbTnX3#00JzRMB6W_KJnYS}1Py`OYNJ}4>}&OO?>qW~jb8+V@VwCM|Fqcmg?|QNJ=0#Y-sQ-j z;{du8-x4|SC-Ox6h$Ax>gJw-ii@lU)J$?#(xw2Mg%i_CY(fNqBjo=E~Q6xB7ztEJE z`9`_#1E0HqxQg`8)+@v*@yZ>US$qW6!u8rp?fbCRZjph)1i}WHT(({h>ot(v#kOjz z?XXcNgF{Lo6Z-4R@vSwQDT}+Mb!aC9I)?YL`cQU?%o$8)ZF+i(Sl+1s%nTe5=^NFB21c z{Q$)McH{iC?hfSMXcsj2ezaZh4R(<{`|ZN10ehtS9$EN3vaH-$%AKCV*^RTT)!q#2 z_sRG`3ao|o>LgDu-@aRFEngvXBBj!5qrMj2FSQm*4;RRk4q`9gEj@OdkDNQroDg7isJU3Z2lna77GEe0U@Kln<^Yy5c&X8I$624tzX~ zJKFq5M>9_ucW0bb^0+lLaRF^ICo@H1{kzI~SUoQt<3ffzdXk#{R#-I6uVCt8>{^@F zdW>Aae#kNEhsX8v3vfiNe#du!1eX61p|9a3|4u~1}>yE_@qhz-TDPs+cpD7PET z218ApqkNpB`iA2t6cvD%+y#?=3ltj%NsQs|yiu@t<42vH1GO zmekg1I?ylO1MqM&Y~u;`K^I8dr*kS6w{*pny0yAkc7HM5e`e4Bg>{xf^2jwdXs^KX(m(u!O{OybTi s%H_?Wx0-{bK-Njzow(F7DU~*B&B=>>ezN>xk~GzQzXVU&OzzO>e>|$l%K!iX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cd179dcd5590fa79290c8aa9308eca91326ce138 GIT binary patch literal 3947 zcmd5|9?|TB#~8LWDNqa&}TH7yG#L z5k&~GOVZFph#ycD#80t%=?Pl#2T-VllpnwcegMzdx99XNIiw;n(VTZ@cV>2Hp4r`; zxYVg#Az_VLqx3i#rFS750iShglwaTNwjuGsKWma<&jcpv)PV9dEuQev~Dx|@5j&$N}Q@Ah2X7B_q1 zj0xGzo|-YpAahrdD5YX+Pvvuq7|94V%iT6~?X=kgA_h`R6 zbLn#R;)fSz&r*MbKRebjy|e~QO|M?}!)Y%HVN@$E`+jpeYWiW}$MY9!odyi+t+(N) z&{$ZTZusF6N8|av^-dTEjp;i3z~%-K%+^bK`8@X}@GD?p(NSK3h3whFF`O)^k`kne z3@O|hqLYG5q>VUj2G7-o`e82C7leq)(`XC>$HC9Wr4Bv2JpDA6bOIwhYM6BFJicS( zF!H2uh<4EL$wc&$Y_qhRkoX<)iX>ST5}PMu4pmph_PGjogruIEnjDs02r5*T_If%p zmbiAs5t7ktn=|%!S7MXWb_CfKG%|VhVfn0^`!P+j9ax+&r6{l8F;v%}JEo-ZDj8+7 zYiw4#BMJArh&*g!P4=z6Zh5_HV4kR8spW3hkR}QE=9A*d^RVwZjQ*+MoBsZ$E1-ee zetQN0 zqM%s|TKoEowUwZO;?o$NknmCYjqV&0|t9@`9SBP zq0O|FNV(@^O|(hfLwic){a;C`Yls5!mF=JRGB`Z}s1?9Dt}QeW2MrP+d=ZDK?s`Eh z-gj5b&(~{>hTcTxN4v*7)C~_hp^l>MDeg9Z)(CQLC8ZSLS?wqR-vJc1_$j*Tn=$8m&k z;y5)E@WfHG9fyxVaZV;#)W@Y1XArrINkR$cT}`C8g)hHpx{zaiC}6%?U=!DvEnI_v zF|J7bhU?`Q$md{>qYmH2v1VE5F|7pquG`|FJ9rbe{JI}}iQ`i%;E9@R9`gG&#(w}C zc;dW>?*{nafC$qOCjO`AzXy+mFXo}Meeg-3V2XKYa|eTG9j3kq11KZ>0Om}*3Vswk zd?z8wV$*TNIFg^=1SA<;6}&OV0IG`Q8$i= 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 0000000000000000000000000000000000000000..bbafd3fddc73fdb7aed94f1a71958e18fdfef4c8 GIT binary patch literal 10592 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 h(wf>^_{`3_!<82`WSKjJ{6vB5 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 0000000000000000000000000000000000000000..bb21debf0ec8d486fa47e796e5379613b62c31ce GIT binary patch literal 10639 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 B&9DFf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4bbf8a08f64c014f6e01d1722190699738ba71ed GIT binary patch literal 7013 zcmc&&|Bo9-6@Rn4=UgrgUxa!oo#)iO=9)@ z1$*5lAe7m=i{Y9UTBHi89FZdBuRucb`YtE65kFQ!;aY@*gpfcg`~p&G#plg>cJ0_H zX%J%M@yxt=^X0wwnKy6tD~B4nr%5=&&M@^SYuD=_4k1rVqCgR46^RR|hb^Kt)UTp0 zgrF_dhg>4p6*Lx*m_|Ja1Z7dbfO;4TT0{LR>cX;U3-zH4_%jxbErB2PV9BB^>aSWv zv%%7ZX1i^zW+t1f?a3dlW~P3;*`Bh5Jw#jJxeoqOA{Dl6S_Rwc($!br1#QG8ddFJ6 zm|DL&R6rdG+gH~1N&7WTnFb&v)osD_pG5pzUyka_0@Eo6eLs!H znD%niwy|-DhyFLBvAWG;uoE7){KoEOrYZgdO{MdDsk^mPy&y)-H(o}?IF zHSc?kV$CB5Q|Xki)G7_XSO$qD;8gc(#qw!#^wemVa{ifQwo1K=vyy+w^OMQyy1mD* zlEWf$TJ+0%A13GXQHwepXS`g&t6`>ijbaHqFs*utr!YriVRmkM{`i8n7^XQaQh0te~$uCyw zP>pFWR!$eoNwd0``A1`S4yQ^w>qW|9GXw;Z}1SS?xkYYDC@%~u;{?_FsP4k57hES6gR3R5Q17l(OaR2 zMUQzTR0J2zs_Pb&PvOl)HAdjngbNWyB?Ij1L8jJ`2UM^bn?hT zqKbw2+Y{)!kR`(c*))6~icLT_1-R`9q!j>eV}RS1?4k?T3IN?cdf-|LwrmMvcv?MU zLW~i(N(asr(cj0~A0RWF+4m5YKz7euS`#UIGggptfCkyoWlI-y#+U4@-L1_-e1!Mt0?L~Pbph6t_ zK7hCY2o8H6KmZ)=8%4%uN5L@~NZMia@!rxLB>wXp{eav7Ic~!|?(u~KiFppqKgn0> zO{}Hy*^5U9t8zN|?QbQ9xK(66gMrT>!wlQMj1piZM<51srR4?)l%UQKB^;SJE|#n@ zsP;VyL@LB}BV0P@hE@oZ5KeU#=XuPuboIu_K2NL5wGA z(^4>|qV!*@^E045izPi|;m~jzCF_N+-M3{_0+B3(F#3^@D+u8G0wwN7-zW$>AUo@f z{TpPqSmsf3RL>ZOcJ~<9?H|)Z%pABhDHQxG_+6z#QogxtWTX$Dh#k`@1WieF$as4G z#gP}jaRZb(L5~R{V>WGq1aX~U$iPa+3HL-x5=TZ}*)QgEez8&>FIW8WdZSvc)O@ed zVd*cB{oKh#bZ(1gp<*>x?2gK#xYYF4JC}+XoIj&u zPt&aXjZ>$RW$V6I?M&NW_#pidc?gI1t0*^+Hvv7Z036`w-3Yi-P{0i0!mc<{G(%}` zfW9e0n-4eqq=ok{D^H~0BcH<9*^ri@JREu@Fb6@I))kaURyM#J(G`jh1S^t(*?U-% zAgf&FNI2Gyd2blC>&HbLIGcBhvr{jGhm_}+JRh;WR&odY#5avmY1w;)Q5x+m8g&lE z50F8#9F&}Q0C!_D+Ai}ZKK4^@DsaY0q1HsU@Sx^U^(o{PO_Snxl8-v67PPU(kR_ou z0J%-K1r0;H(AcaB?8}PzJ~@vIRBMi%UA&bJbPO66E3bHbbOtB>uJI-^giC!XD(Mag zscnH>q@*NQ=^ct@OCr)hNTEnkWUjQ!GubSeF8`apUveJrtA4bKRiFJsQA@?r6x66+64 zKL_;^vMp%mT_~rJXQ)MiWZ*;1ihV9WqFD8oN-YI+cP26k0B*Z&*v3+J2pU!sZ;|c% zL2>`b=Qm)AO+2pWm2xrPd%qeZb!=ut**Bh;^IXweEMTI+=`zT_ zLQV;~gF`JADXSC^1#w8ybs25{UC`NXcqg>a6oGKC*j(3YZT$;sjn)x?740vy=uSlVJip!(tqjq##wDXFHcR{g z#9%F7DdP)-KM{HBFMyX*sj=w8DcAf5OU1JuZc4-c`1r|6qg=?<&h5XS94q1`RN45P zPF4zdC3C>Ssao@LCGCZb=_C8b;V(yB=BV6w3+x=3m`HjjCU%Z6x0%|j7I8f{yl9~T z|Gx%}aRB&lyC3BgauXPNTSgdp7459TMJHP^El}{`N~sm-O?xHi9e#k;T8cCl02&7< zNeS2DKm&n`;6P(ketiCI%r^>x>6hRea|svmN9WngjnYTw*&eK^TeZi1;N|3dXZ;6t znDO{HuD=>Q{Qmok^~%Jfk3RC)#NIyr_i8(uqyPUmAiakS4LR%$Z&yPrq_qTx;L;j2 z_P>*|SFT>r^~)Dgev1O%e+YK5??TB^vKwqlsqMc@wP+N^o`P)@`m&H1b+0bK#-<#nZ7hj?d2C?Ru+gi*bi8S zF{kM2V#RX0FP(c}JY5)1pBzs=JDxtUFFm&}eQZ2Y$oDJ!$=3{yzHAx|oSoo!64!Tp z$WP!i>#^B+yzYzxD&d3e#LM`G%bIdn*PQn5;je^))1N{9J2Fsib0p7dQ^>5g?GSqk ztr^*(E1)?L8HMhb@wUYWupdYF>nItb+-Cc+M6r{tdG!W9dEZL< z2bexV#vM7l3*{8DIXIh2QYzHgvo7Kd%opRDQsSBlX(n{3DKZGFIW|z%HQB~xhC?X1 zzpXFJYppiDkEuVvoV-W={vD%2e=&}CT=#0Qchbvmr$nBP&K+~8I$ytyNf0r8%7^L( c=1#y?&DHC#RBBzHu6sXZ5BffiZQnHg8_N$b-v9sr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..140071a3f9cfd18d49c65a4a95b8de22ec45227b GIT binary patch literal 3092 zcmcIm-H#MS5U=W)TkivW(!IzXmosVtiI|`cH#CxH1=+P{i?dEy1J&i7e46KKOuIf z-D%oi)XXZtDb$misBehY4e?Ekvy^BT;~z0*W_pD2EEC&3GBx7X@i#}Z;?79c+0!4M{P^5i8tVB-ms&PA z!`4>t6}bn7QZ5H^CkUcey$RK0U>DjfSm^V=2>k7l4!_-O)>~WT%1KMbyS4Gf%nkQv8Kh0*f6OzoKV$}`GJi?%BaUc9{?M8sly4;iZZmim%4~5l|jI| zGW;kbN^yLejI*Xk>$%1x>*r0z)wE(BLJxRZGr7>_{1;s?(Mg#c-ec0RN>nR(FHb-ob7@Hv&SvaP2eR(q5VW)@;(&d3&LST?vxNqagYg_U=dgP50 zN6CE=5~YN#tMx{>wcHJ&t3k9J1)T> z0J4F(>_udk3j8NRw2WJ@Oe}8$eMcz&ll*rf`ZTJe=)fuTJE#NfVK*hdr)_*Vh&3(e zg6iU&DxQ`(E%u4e8?CD8oOv3bPX(W{ID3{K3A0L$Sy9BRwM^oYQ$_B2Vt5TsszPWU zvx%jxC&UIZyMg+0A9VWb`nWAsU4eXJ z_^+U@_A_WBUI)Ly?{L1xo}*;7@pGKQTuOYcgRfX3R?4Mb%a6l$Ym^z{`o#tqili8J z>S5F!r5PKw#qzk_>NPJ0krg(fJp=4hRCZ|QCG-za2b9YG^Va-g!#1Db!>x9%-!JJB!Lrjb(e*kVm^aB0-8^ARxJy|cCH8}} z7MjqW1$JI=ho*%~g72XglZhXtCN~`xGB&6#px?mZPWq^x&J`kZQuz$v=R#04RY7+P z`tEAcX6Spkukr3Us3~_*RnM;O5X3VoZ&T>4Eglkf10=$BX_0un)FG8L@i0k8`GQ-4_ zX;m2}8;O};JYu;ZAK+{US?UbD%vsDx8JLnHwcMAF8{sW3a?1nG-vC9xZU;f>0$+YlX?8A-4%c&d0ao7ynJv3{1qf*daebJ2p))W3 Iu%F!h4dlAExBvhE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a7427819f9b3097a267e758a0dc5c89b402c7555 GIT binary patch literal 5952 zcmbtY-ESOM6+d@&c4H@iDpg%qX_Tg|x3!(xPRbJrm1peDBA2v?mQdu6wbmbrSGB$F ze$WIYd}r;h6iVBtJP=%Hg^>^vPpFTG8OL3T@>*W12niIP=^sD?zjN>0nb~;DTIH_p z&bjBFd+xdC>z-wP@kBz$ZcBW?OPOBDH2mDa<^0D-APf9 zvPtA^a(8U<4r~hSG&W9?`(~QFkJ1#RGxQIa`gT1M_dMpST553LfRg$}^JjyW7GQZY z6okm$YcebqVZUd+y|-`Xu*Pe?y=Pfw9)PqlutE3E-X!By;{_II-`Q&_+7#yBJuOI_ z2H(dq8L6_m@_M6OT_O9JUP-ljwY+hS>{K{j_!ilj5U!UN*OH`_rPo&KWU~a^8r3yO zqG4M2{-u{+nf}g8WFHSPS6;q+=KR>Xv9HIQt<;uEo8V{!AC19<7#xeiCt~m?F*p)~ z&&1$R3=YTOg&1^V@aY(QJ_gUm;9v|s8y~Wj0OMt>3=uAF8q=g|w~!=mtYhiDL1XH(2cvWg&MTeD)} znz~)9H5SQ!C>#yV7TwXA(&&%QjecWcewd7t@ie>06X?7Zo6l_JIYb8-x1|qfBDxEb zd5p0g#FNBM6M53rN_D*i_b{H7%jM2JcRdzo@dgxel=du$oZA+zb zsDL%DRPZG={!J`rS$I3Pkk+`Q8NhtD(>Q*XESZOv+34C5^8c{J559yYrXkIXp_EC# zkif-CVK z0Abeu_f|v?S7rJ`H1hM9#sf{zo#rjlolYp62ORiz;DRnVfxGMhIzCO3 zAG;g}^;f|>${|Xr9Oh`BO+;v&T0%UHkH^^(pfSjJZVaP`^n6%tWT0%5D$6}+W94D-|`(;LVzx*b(ciIV<~Psu)HKGK^x%j5np=^^SA~008gY+ zaaS^4GEb#-mfyDV$Bh$rtaP?Ew}fi7{!NZ!b$05?=;ms91J{vJ^Z1}<)aCzO;Cytb zrT;ZNe~Ny9-iC`m=1R&E^TM6dr==9^~V-r#!NJsH$Tg0d^1aCFfste3io$oi`QoNzo}!ij%J1 z2TmYvlGn!u(3n75&QyB7jpAJCW3yoQbVbc z(R>JX$5Nu8D&}T~#?B7s=CQ!sJB`{33f-%fcq_%XlPeRE$t2AC=V6D}SbT;&Ixa=& zI;seYYc8XJ+S2OEI_f=TEf9ZU`vqoTK!j5d&Rvw%lll8#{OhAIH`=*cEnQn**?>xW z^8O9@{`M%mjd5OYtW;s)o{WD7roTT5W20--(xvs<5jg(=T)FquwNXdxEIpZ&a#C_dR6>+o*jE=%45)VvcVL0nSn zir~W46fTpvRMF`#vQLyYjVD6|s$GmzTNpiNW+KjCiR@{7OJ_wh(wDzknRpSNjmx$9 zgmB6+jXTwmVD*;)gNcK}Y@pI*lwo5BG+QExr@|9)iD=^1wFm@JpPu3)+4C9(5ai8OkCSA%o z1ovYzo{;L!R%5st9hU|8!VJ5eq4E0|C-8BpS%A;YxICAUfPwRx1uw)=N&;rO?LrXZ zeBc5_w_U)OD798v;5W&1JM7wdoU(;=x0x@I1UqWuZw~bY^HSmA>6&2<@*yV;Y+_l4-1;)({zoXs^ry%$< z=qY><7=e^8pr3TK5`d;1^yF*eGLVRDU?uH3WO+W(CxAsj^gbM5hZQN@RNM7v7mAve zp7kr|{+8zBLU9OQ(DGZ}E(+BF>8hf4+V{6RXav7$={K?j&5ntErD({aTcmPbbeS^4 zybOhrf|o247xC97J7l3g=$IfuM41w)Tw91fH5opCviU`h8)ldB&2On*!95&_gNZX3 zaJPi^fIM&o@}W_qQJPrI$s3kpNA=cwzBfqw>CUOyOZph3BB_ z;2mh*d8n=h{&P*fJ094>`&{pVpyNFdY}CR&97r8|Nhk;NX0MDJ$^bmS^f;JfdenAG fckJsncS5dOAZH59xE7DTOi0S{I1YKk- zoHR>YZ5>#W`Tk~S4{uU&55^5{V7*HGg1{|04GTC@V9gA$F-1!OO*)P>MK!A^J8R&R z`Yvf&iV?>O&N5UJCD_)Cc!sGekw!5mn~@L1m5KYpM3vFCD1TL(5w=MSzOJqa^mjK6w7(dG-9zTXvot*XhQ39HPL`= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0f6174a816a8a54e2d6718a9528dbc1e38391eb3 GIT binary patch literal 258 zcmb34DNPJyU}WK7;b6c7OsqmcK~^AkbYNhRX<%S<1+p6(7#L!J?6w0A3L+#5yHac{Ji4Q K+*GJMf&u_Yz$ya( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea4dcb19b2372e83f69c683005b0a2bb0a6763ae GIT binary patch literal 186 zcmb34DNPJyU}WK7;b6c7OiZjmK`tOp0b))Eh6XtX21YrC1_nkTAFd@iKd-nnHx(=h z6bA!lAPLe86l4a`u0ZVOZ~&;4fg#2LVh9tE%>q&g0{O)V15+zflS@ldArd%903VYL A?f?J) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..886f560e55b7d61fd8e26d258ae8a8a390f8f312 GIT binary patch literal 259 zcmb34DNPJyU}WK7;b6c7OsqmcK~^AkbYNhRX<%S<1+p6(7#L!J?6w0A3ch`At;v<$}P#yE6q(xEkf9s OT9KMuT9OKtM^FHVs48Fp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3e94967def52041319fe9c119e037e7e5763340f GIT binary patch literal 349 zcmZ8c%L>9k44iFO><4&Lym{?6^dO$R_yG!~U1*_oacQx~`sZf$0b(FKnIw}<_Sd<- zLFqyl*dbWe*yrTzFc?=3b6rl$6f1-*+sJzYR2#DoiH}%4Q!z)##Mvvkyom8x@UTK% zstTVKxJy~|pgtFrHG(=wOy@O!tyHI`^e3~=GdT^5M?##|^cYhk| z3ydvnfgKjO&fi1Tjdc~K@YR&EY{hrWXc-%axdcp!8HU`dDfr1ZQCTSGSXdE*e9d%V z9Nn99z?JHaSzDqnClMY!tjxmD5B?P^DRTUW*2g2F3uvBgtkO-&sjqLulO`^le-5z^ Dqu~;e literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e988dfcc5df75f5bc3bfe51283074574f2283f8a GIT binary patch literal 316 zcmYjLK@Ng25FDT?H{)5u$un=jizhEWKog>kHW7$GBS-w1Pjjb*q?^p{b~-z|+mkKN zh*YXlY~Z7)OxKzEk-u!2_d`F?EODGj-U~7y$}mbNjmtiUh6`u3d%5ER6ep}OOsP#n5msHK&zJ#Y6Ll61}wek;qo=sU9_n=c8faiwby5K~J VvHPE`sHJ8(A=I|EcC++n`~kvE9hU$A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5213e606ee5d09337837dd89d557014293dc6ced GIT binary patch literal 282 zcmZ8bK@Ng25S&6a=*0(^@C4q_i=Oo012iGfD2WO&CE>`^e3~;wJUGdAcV=g|yFK*f z3C3nN!v+)FtGV7naqr6_4B;y(CCP&Cno%=W4r30O93u?5Ra5YTuc9(h&ato}2I-P% z$Jo0!=YTWS3$wOFUrHi0Ev(E!*LMCHD=BjPht|g9HQul5&{9KKOdm9z)-;uUE}t)mhhU7mhz|Xur7jhrVlgx$kZ10aT+$ zsjrf2Llgy1G15iw*%&oFi=-kM71hfdscHPn=Rzy->T4+oZ#PVOlnEWemb+vq0x3CM zWl{mb=K{D0XZcZ%U|Sb>od}GWf-KER9}0%wfGDcq87_o`fQBc*gCXFEW2#UP*aL2d z>$c>b;H2ce;O~Tg$R{~#$@B`ryy`UULFmtRTDMEnA^!g<`k@cYYxZniYK4mU2Et4+ A8vpE~le%-N4Ge`C0-KtH zp4^<1C5WI5HEAFu02)?84K4%+9>GHa@Ip*S6>`{`ZS!ZThGmMM6^3GQECN{FUBfL9 O(`VeI@rrU*(i^|Yh9{N) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71021c22082e4152557d773429eff344edeacbac GIT binary patch literal 398 zcmYjNOAdlC6nsTc^a>`f+#>M?9Kel7s8C}gprNf{$w54f2k|hzX=&ml?>C+INw+5_ z&m_`3&FM%1l~u-?@~TJc24%`iU0A7mTXeeDootJzw71ec+)%4S>*U;qkYqXgJzG-| zO&s6TL?2{=#J3``^DHa*Ekbaz2t|U3ddC4;NKlC4veat0Ro E0g#0zMgRZ+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b0daecbb3aa2691567d0864541cdcd81c4ac25de GIT binary patch literal 194 zcmb34DNPJyU}WK7;b6c7Ol-VBK@}i&1>%^728IS8eE>+#X<%SH0Aw>dGBDf$vO&;+ zv4K$zgn{x*KsgpRAOQlIc?G2<2ySvtYGNKx0|J29EFdl;0~0flasc8uAP-~`ApiiB CWe`#T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..494e02b445a4905aebe13e2121934d7e37a1e084 GIT binary patch literal 677 zcmb7B%TB{E5FDpzY0E<(aRc=eD&PxTiHB5i;SNM04-qvFh-}I+{8{-kcAP|16&FSt zk3F{6yX&)i8$Lk!V}FcOw9rvin!3vKq@L$CoSRS6?NB`LM8>HGmZB5gG<%{$;Ye%5 z&coMm11z9$-ay4#Ou?O2xDh_;1~W7miRbeok=rQyUA-Ff27lUaA+74-Bg}0A$+AVA zyxXkaG77U=XQ_l>HpDu4iHaCryK>_|M)#hQDr7zb6cgz|&lmEH>Qn?}SR_Qe&1_mK!2O;ao=YuzoRD~BH6a1 ME&dgoNgfM{AL>^{pa1{> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7fa65fafbfd53ef6ca1db619fd1d13128b29fe4a GIT binary patch literal 444 zcmaKoK~BR!3`PGW1EfVPSR?fa6>x%VXa#}=2Y@I_S}7`-bdjiChP!YOj)q^FNQebr z`s0~zkNxGF=PrMh^NCM%trOC^-5siJwfyMvWqWK#nGnS@JLkryRQj^`*!TU~VrlPL zs?UYw7U~t7<~cys$e*9C+Bf@6ke`NayHI3Z*oEeh-DY?9ipDyEHxB=tBJ`;_Ie>O% zq{bPk2Sr^D5$!iHe~I7}^XI|1O25sn^k(Y2MaB^DYz^6BoB5)V4bb|bff2t|7Y*Mb6hhU&%Ph1_dkL1QBF8_q-fl1#aIQAGYx!LU(MOLIiX_h9PG?B;8rO0%(yoBk(#3p%wazZd=lys}7qF8jtAG17N zhNhGZ%B76bS+~f`c}=re_S|A9^Gz^8AJngXl99opc;`^i7h^Bv`oSihy~EZhUu*bQ zBdDBVVrOgA#I9lG&Yc)_FXN9<|1se&Cby0BjpDsc>vI;4nD|hhd)MKtNE5 zk)GdpW;`<)J15sRzJUry;Rw5EL@#q&BxX9d@w8l(-bxDkhq8_}a3vh+su~LS1p}>- zIvZZ-PX&?2>#zpSD%lGQ;jOArQ&Ujy8fqds&xc$nbDKPV?H&ljFb{a#_9p<3YIIn?I*DxEeT(18P>Suk{*!}~)kER>H?g<}ISLh9d zE(DjlmTrJsvEB)@H#f+2w<2+1g8g5>0vrs7K}qr~ix=svH2(RyD-;Kx9|OB!(e|3j y{6gLku_54BvA~hXv2YhW(-tmFoX&i8XXL?60j02uPaj7}UwiOd$NZ7#q-Z~%~^#HA5wQYB8+r7r?24$^~g7~a?oRM3&e z?~U#EvwZv5rB6_LtjD-PK-|_{lbKoBr8B#=`(2u7bSdk-Fpq&x2kZcRIdK%!Tmeow zHA$Q(ha>?e3Dgu0k=)ZGSqPgpuf{?TgG5%Ew8CJoXe3jwBSxJFhjEOdi;?VrJGnBI zJ*L)TH7^ilmleNiY3&;NEF57`H>*urb_SX;M7GMkWev7?jE8;s%d)VXpGIN8Ddxh5 zcqiQe&(hnA*q^cc1a%8} 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 0000000000000000000000000000000000000000..9672d77f6b773d24835790d977ab7b9a5264f6db GIT binary patch literal 444 zcmZ`#F^|SKT^MF=LI1lFIvBcb UdRK?^H?*?dv}1R1Y5QzF0SF!~S^xk5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7ecd2a1f96b93674b93debece40e9b9938996301 GIT binary patch literal 417 zcmZ8d%MHRX40TF@0wW-C;##^w2jId8p$$?*)JjbP9LpXI!ypWUU(#^kN&dVS+i|iz zdU+y|7kNP?Mbu7NXRC{TI6o*`otyHijVb$^8t#L3sE~le%-N4Ge`C0-KtH zp4^<1C5WI5HEAFu02)?84K4%+9>GHa@Ip*S6>`{`ZS!ZThGmMM6^3GQECN{FUBfL9 O(`VeI@rrU*(i^|Yh9{N) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36ca0e3996efc49e7365bf635488882ae507fc29 GIT binary patch literal 441 zcmZutNp1o$5G;e)!X@`eJb^dN4H0qS1;~@JA`~k`%ov3m9Eg-hgtx$jALKu&8Zaml zWtH3A?(VYtX%gKO#&&HNk2oVM7jgOGhatLQ^;un_G7V+{hAiX~8?Ivp{4l(yKcL^B zwM}k9{D5trkEmQ8d|iGCKv^Bq7_|Dd&lxag*B$K%2Uu$TVr#w+*edq5$s}VWqgrdZ z32%OV)0~Z9(`Vef3+r-T&R)YJccD&}k_yGv9qPS_dYwL${;+Z(diuZvsT-lHPl{+b g>M4}o7f2IRBe8dfYV)s1U6h~4A zNNOQcm?X%2P9SDvVqjqd(k(zdr=fwN6KKaV1_#D_P%}X0v#6h%fA;8N&bo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b75dc7cf2ef6b6d7a05f7ad412a451082dadc724 GIT binary patch literal 538 zcmZvZO>V+45QU!uEznjidk`zuoFND3rUz&x7qUP^K?;e)(%glEaF8CR^Vvz2R(;a5 zXWmTaCtkd^{!MN?9Zxk^LVh~7^|siQt=}}~rc=4H$&;OH<1Ux3LApu$3T8OD%0z|AHR!EgH`N%cE|8M}L^m>ZKVrLsr+rWb*js5lqu^AG$$j& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e1b485b1492dc775fadbda44d14c777facd5fce4 GIT binary patch literal 151 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owkF&P<{Kr}lLy8>|> dR6SUp4=4Zv#Tki3sVVX4Ir&M6IS?TnBmiuR2|xe< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7d26549a70f8a433a77d369cd6b748ec64b223fa GIT binary patch literal 327 zcmZ8bOAf*y5S>=+$CY~%kI*yV-i;S9!4{RIrKW&scX%{6p350pV&Wrj2HwoPVRv-m z1PLox0b7{CP49+UR;?429qpo0O7jgy;sN>M0Hycv@f@-@2cCF5ke)H_!2`n{w4E_H zZB-|K!b41LFB)Lk(s5Oz#Ah7nlrc)*LXkEm$P~Fku8}#CdgG5`rW$P~*h?GcO1wv{ hnRM_WEv|lw*2&;ZDmfGCdvBx;%SitJ7!8w7_yRX2B>4aU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a759c66168c65d285a98b1e1588797e50cde352a GIT binary patch literal 364 zcmZXPJr2S!429h`K*hw4#1VQ1DlssyaD+hJsZgm+1Y<8k&%)7oFKq_E(PulcjOyXyntsQ^DNW|pv=D#F6)e20;X)@`R@4yG`jVr!>em>{mi gf|Oph3e~s^tC95&?C|x0$#E38a`VnQ4J*f%9}aFEX#fBK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ae966b7a82477fc3d2110d6d8bcda58d808779ee GIT binary patch literal 443 zcma)0!A=4(6r8d(0RkR8c!Br{enUx+MB~M)F_=Tsk>Ml;JQw-R=ASMPFXQ&72Jt1|UQSHr-q|VhGP&dfXY#q)8_mS4Y zK@aOic$Z?Zo7>&mzaQ6DW%#sV34PV9HI3KggWK5XmgqDSA+eCaN}=eJ?}$viFyb@$ z`U`nVo*4@1n>w1@&^q&g0{O)V15+zflS@ldArd%903VYL A?f?J) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f3a7700e3b4c19bc36b636fcfb1cd594292f62a4 GIT binary patch literal 294 zcmZXOK?=e!5Jmq;EFy@YON&SF4C%t18~1_)61C7)4UKNoji>N1X5%S(7^kQ$=!d}L z@h3C0l~Zew7>EJ>(~G7o`}`6K#2&O{t~{_|zC9!Y2zTbE3}B>MGP9?OP_vIhfFw_G z0-D34+0w4E=Il~yAVzGP6vmZSWs5^uo*>3=+1fnSI^*{6<9A1!UsUE;eUINeWwp_r a;Lep*H{^=8GwJ?mI$Kv*lc|!&fAa!ipftGv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fbad0f44d4ae5da2addfca79dfb080356911ab7e GIT binary patch literal 239 zcmZ{dF$w}P5Ji91Q5W@3BXA& M_j%p%I#2BrCkwt0hX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..34071c05be4f328a4eac89a0447459629f471e8e GIT binary patch literal 237 zcmb34DNPJyU}WK7;b6c7Ow3$BK~^AkbYNhRX<%SX0kYj27#Q3RI54yV={XDy3^71C zIUvmfl!F^nlv-GtS(M7a!Uq%uf!x%h^wgC2+{~1e9H1~eM3kX8zqBYB$Ol;h0>!C$ oDGZDdHkf1vl2Az|5E~>X2c$qYg4irt<8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5dc08efe2c198a25b2b22095719f1f1af6cd3b68 GIT binary patch literal 381 zcmZ`#y9&ZU5S)vr_yV!95&V`kb~g5ch&ePE6Y}a;Ec_8a#6tZD@nf7NYT;vGZg*#w zy_*|O%Ec7MHfv#~&#=6M)zV0E=3U%Mzu%cS67y^@f-IDq#oH0a)jCI+7BulrQ`2X)jZjX}M~s2+q= y4|~cNAGom|_z+=dz_#|!@3BXA& M_j%p%I#2BrCkwt0hX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c20c3daf3f934da75a935ce860bcc32e484d89f0 GIT binary patch literal 333 zcmZ`!K?=e!5S*=1sy;#JTXON{#k(Lv0|slQO#>cN5dDP5KE$WFOSB#&1Ix}1%k1oG z;}2Vq%2kekWRoR}3$8U+uYp|v4a^?_7D;pl_ZK_5bV7%y1rARZ*S z`~4Gg<5{_yLr710_*JVjBJ6NQAuzEJQ~IL?7W7y8a|N8ZF4p1>fhU@3BXA& M_j%p%I#2BrCkwt0hX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8aa0575f8adb6dc64570b454fa0d0916e71beb6c GIT binary patch literal 240 zcmb34DNPJyU}WK7;b6c7Ow3$BK~^AkbYNhRX<%SX0kYj27#Q3RI54yV={XDy3^71C zIUvmfl!F^nlv-GtS(M7aA_x=(f&8-6qVl55lGOOz%#@TIpbR@if}uFSv?v+K2iXGx r#i@BI42%#qm}CZ$P)Q~b8zd(Oq(GK}*eq;70t7Pi3Q9{LOdKQt#Vi@q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f46bd62fa94979c8e2e1f2422b7dc9a0b352624c GIT binary patch literal 349 zcmah^y9&ZU5S)#tLX(skD>Q<1W3f*Q2DOPT~@`pgPE$4NY5_6 zIIiqN@km2m;kVCspc#hMJHf?J@3BXA& M_j%p%I#2BrCkwt0hX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5c08366fc72bf67227e12bf36cbe8ee252b84c56 GIT binary patch literal 241 zcmb34DNPJyU}WK7;b6c7Ow3$BK~^AkbYNhRX<%SX0kYj27#Q3RI54yV={XDy3^71C zIUvmfl!F^nlv-GtS(M7aA_No#f#Ss6)cB(O(vsBp+{~1e9H10CM24X_zqBYB$Ol;j s0>!C$DGZDdHkf1vl2Az|5E~>X2c$r@g4ir02K`_ii9F!i3mtUj@~H{pPYD{B?g5LwwdEX8b81beX_cBw5v2xtB85a2 zoXsvxirnn7C_X|@3BXA& M_j%p%I#2BrCkwt0hX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..533276eadb1e4c330c2caa84022fbe2956240187 GIT binary patch literal 255 zcmb34DNPJyU}WK7;b6c7Oib)RK`tOp0b+L$?ZCjm>Cn(1$H2fS$I!sQ1QcW81`;4p zoRON7Q&^f`lFGos1`%RN&Ph$o11p0HFfaodtRM~}0~1I)%qWl$E&y`|P!BUmuPYEY W0C7wM17jT2DU3iqHm3lEaRLA>;1f{* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ef72c48ea42632ffcadabfe84311af873555b043 GIT binary patch literal 304 zcmZ`zF%H5o5OYc>-9W5VeM>vAv9Keds?89ALP?v2DGZ2DF!B&scm>=i)j~-Z&zLGxQfD`fJJs!9t%rc|`A|^glAc03n zef@Z=_D)*`IVG1-=SDaBpvrZzRk=|Xa@-|fQLW7ea`f?c&K5L+7h}}Ql#u)lqa6d$ bUD_(CzSCepnzMusUXFGDRGZ2*Wc@3BXA& M_j%p%I#2BrCkwt0hX4Qo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d88e5ce62bb6312fb0a656f2c00aafa3765babd7 GIT binary patch literal 542 zcmZ`#J5B>J6dW%G5}>35}e+cH0|5)0;?9X{@HoIjIS#u?2i0n$({~xjgL-2VpEV04XX`}fjy+ZB(Cv=_ZmEbPd?2VYdcln zp6$QsDS|3ZcOZ4+TiS#EceXP1&-(fx{f|~r^8yL=6)GiYiIa??;T5+jQ;MyUB~_Ad Dc?dav literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9c43efb9e0924b277de04501bc4e886ae35a646 GIT binary patch literal 587 zcmZ`#OHRWu5Pc3NEg*J$7F6X1oFQ4jiVgcpRY)zch@9di3wG{;TW}b zm}kHwKwXPo1z;xnRrLBDcoY3C`V$Ht!mkjJ=DEsLP?H=g&DXeDmu`BFM@E}}7RvAvb`@y};HE2hy)#;y!-n=FY;L`SkFk~M65Fz`@pU8r^2}5xFmjx{+Qe^hn;>Ww}o}BS>Sl^i@^`CBu>Zx literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36ab1668019860283671c1b5c0331aabb90cad77 GIT binary patch literal 235 zcmXv|Q4WGI6zgCLdIMs@5j+D2&<{>PCM-&_fI&y%A4m6f8%?^`*KW-&H*Z=fUFs4Q zBAU2-jfXm^11+TQViNn*D-GY)1CEikXPm1 zRU0;Y?GnWY1(j|6{7dV1X+!4~EZe8GaSn1=0^49#_5jIUad9GQANviDEY8%Buae}e k_*$yClfSws=d_?7jKS?0?C0VX!`f@q)GI|8A=*`g^%%R?f?mBV&NpiF2n5H z9Ye#eO1wDjsd}j}0%_g_>QaZc(6x?X`9)}032Ot_ zOdsR2ZebnP9nmc?wh4jS2L^ozf?R~Hm}`aIhz|d5hyT(if(7&U8aN&f9F6;90@|b{ LzTtkr_9N>J3=b({ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1365c557342a36888f226a9aa6eee5c633fac960 GIT binary patch literal 374 zcmZWkOAbLX6g{QB!3KyWSVIlWOvKcrq2fioN3~-3Osv9Um{^6yxcBKxgp-`!)05lY zbNkbvXArU}o9N&JbLQ>7 z#7pI3h#U>-r4b)Ea|nquklkpcCU~dqXyg6<-#sSj7P1|C*O}X$v<>vU9<6vSzh7nZ zjju3h9!PbV=Uxm(}P|L6MXVUV$4KMNkgKW|_w8B=u&6R`jXh E4d&l4)Bpeg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a661104030331a7b4480e21c6627a2faaafffce3 GIT binary patch literal 161 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owmF_{@SxPa6d2L=Wn r#siEFK#2y%28IKS42+@&8krz`2BtX12Cz9GeIT=e;DbGc#!dnNcBl(j literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..28c61aeae1a6e1cecdda793555ef0a6ba95f41ae GIT binary patch literal 136 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owqF_{@y*nrdlpc*M4 TEylpmZ~&<19+EzwFirpf^xg*4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..18ca35f7edcadc70cfc9bbcb463ac6c41c051e93 GIT binary patch literal 216 zcmb34DNPJyU}WK7;b6c7OiZjmK@}iQae&CiF)%R7F*JZVOb{V3$p|EwA$%qf%>~4A zNNOQcm?X%2P9SDvVqjqd(k(zdr=fwN6KKaV1_#D_P%}X0v#fC4xH0Ajxp%>V!Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..59c6b64edd816a60fb76ec19843daea26a9a0516 GIT binary patch literal 208 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>tJ9F&P|hNHU^`iWY-BruT%rH~Pv!`x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a52acb2d22f6a18779c122b142cbee713d6511d1 GIT binary patch literal 196 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owqF_{^-MS;|92L=W) z28MowkGnv`hfOIMY0}}&d V1A_x2kOV?GMg}IBK8OzNBmlT(1(5&% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71bc5b9c3d92e1e092a321b61d4c89859403f748 GIT binary patch literal 152 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owqF_{^-xPa6FpgJIA iaA0g;XaKUr7#JFWVl0Y{3=Con|LgA|nFCag6953mT?xVf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..43233c9a28553c363b5d9d74b8ad956474857f8f GIT binary patch literal 344 zcmah_!3hE}5S&e-o{Ct2axV)>0e)=YK+qEW@546qWg+*l4Dn?l&O{W!fCDqTnO%01 zEY^qZ29}!C;1?l{X|Bj!1}+XfdI(k&jyv;~QAkB!qK2r{5CTK{)hD}#0nL#LhNxMY zbh3kgcJNq*?l~=$7_@g!cI5kRrBsm_y*U{Jh7-mkdDT`v;O6`ogJp9p8BeU8$$vB1 Ff+qrI5%K^4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9f38237dfbf562984154b9b24f1d9cc021e73074 GIT binary patch literal 177 zcmb34DNPJyU}WK7;b6c7OiXM*K~^AkbYNhRabRFn0kYW}7#QRj8W@;?Y!-GP0Rp-C sDWy572!3&5ZUKnT0+D7YD$QeHgt9>tNHfS_pdb?ykdgzkK!y?m0Ge|PCjbBd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1a3b002b1443f24b0e2d58675e32838700a3b7bd GIT binary patch literal 293 zcmb34DNPJyU}WK7;b6c7Ow2q$K~^AkbYNg;ZD3$%JK(@5tHzfssLup@9Kp5(_(!0D;{6l+v731iv^jw*bV4SyzyloSm4CtROWtF((H}K~ZLI zVo@an3me2hhQyrA#9{^(J}9>|FFP;4JTD$93^Nxh40Ij>Fawz&&j1CPn1GZVkOguu GApig^${`~F literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ee28aea2a8f96a33d564f343c9d5a5e351b17ae GIT binary patch literal 207 zcmb34DNPJyU}WK7;b6c7OiY|WK~^AkbYNhRabRHN0!w_6aV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea94c57eb9221463cd280f89d538576cc2c8f406 GIT binary patch literal 361 zcmb34DNPJyU}WK7;b6c7Oe{h`K~^AkbYNhRabRHN0Cn*V z3S=*FU|{fUU}#EZU|?(o@}B^4=Ya+WIfe$XIV|i95RjXnQks*>0OuDc<`#hXoKX3U z#JrT8)S_Yr78a;TeohJl3&<@Xkds-K%D@a^gGrFNV7-hCOiVzw9MAzEOYs42pd>Fy R0SS5pfXYFELW&-casc7_9JT-e literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0157652b30dee0297e988ab9fa1b9d7e57bb7cc3 GIT binary patch literal 248 zcmah@TMEKJ44kadwt57mXW9Py;}L?pD7dYqW&amJ&*jm~`k;az4kXM>5+>_ylpTmm zF7XR9Td6tMYAkR~!0m2nV-qdaD?mCUIOUa1ml_ERgXXwNPNKj~50KI>0ZFZ+`m nz8Acg5Wv;z9!Ke1@WTk^w=={}6DQ=~o^b?Yl-B^|KmOqXKUWwN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0a9c2f928295a044cee2749c4451fc71b9de5a84 GIT binary patch literal 157 zcmb34DNPJyU}WK7;b6c7Ow1rrRv>nCU|^7GU|@7>aA0UWz`zj8z`z&>6qjRYU|<2# zaP38@g{7HAsSGS!Kv57VPRuRHNzKVjLa52jEH2K>OJ`tVhbUnv&&W^A&4h9g6aY|s B65{{> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..914f8fb94524d6a1c57114c0638fbd355f0dc738 GIT binary patch literal 240 zcmb77NeX~K49koamoF%ue2Ct?_yk1{!nmN!-KX22H$gDaHVsLeHNIn|g>m((V`7hjTE`A26`g3uV0f M1-?sY_>rOg0)Y(suy_OmyArDM@vD`fPoVo{v5UuUg9!-M0OqwjkGq2

    CV;>humR2>i?v$!2UiL$AE{`ePt|pJ1Wv44R4tWSDS^EpJZ72rz$gBtkyRJ_$ m@8;hY;`xLT#fcOY+&-TbV{NW>?UpKs*}uy{fBr_tSKI-t1R;U| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..01c7b952fa395c251b64e92381ff768507e2be0e GIT binary patch literal 913 zcmcgqyH3L}6ul_{ig^405>sa~vt?mpVMmIh#)YU(0&!f?Ee!N4fNv>F{{p^+bKJO~ z2qDD4m5%SVeP74d#}_uelpq|2LphVR#M-7&Q&rkTjWUgiCi#71Yh|LM%B)sNiBpv& zlU(U68K+vS`V=u;IMraVi**Dz9YX`778*E_R<=YWROo z<=F#kvrP~B^x+@k&m{Me^a#LHZ8O;;@EVIeVT!9Z4pwm+aMW*@;ydm(f5&(H`za#Y taXNtZIE38?+u=OE=fp@#_a&35n!7#w`C+KM;*a33B~K=2p$&i7ln?7IZl3@E literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8404c480aaf1ea0205b517d24dca5582cec86ad8 GIT binary patch literal 381 zcmZ8cK~BUl44iZWy9-YM?Gxw&xP&_wUQk8E3l-kBN}3hN3JKoD)3Dn{D>0Je$;4wj zchA|sz}TJL;SmcQXJ3zf|Mjj2AipJR@c>Qf&wQ`3n`p*gn1p&4R-ZUe;!ajgwB zRhwHmuOyxv@I3)Pzs}sd$=ryxg^@RdAbjtpuLCAr3%6T26w9Q`J^9nX1le-I;9o!d zn+U~_h&(gwel2+SU0{j{)~-s-olLwycbnY?RVDlY7~P{lHQh;T)Tdl=_MB;ErJMd# P`Ty96rcox9EYgfWmw++R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5e3e676ea53b3ce61728ae92e41e296a6fcb5fee GIT binary patch literal 284 zcmaJ*K?=e^44iBuTm6F4Qy3OBCsy)+RLD?z>Pd$FdQ34u7t@Ga*AFqo^Wn2{3ULS{)b0#p sy1-5H$pzUmA#mnbRF?A(7C*NIL1DLRR%Y$!?Ah&J*x!G?o}l#;4?e&Y-v9sr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ed289ce75b99dbe0765feaf413f32983d554c035 GIT binary patch literal 330 zcmah^%L>9U5S+wlTm1y3CqE<}^yI}4PzW@j*oU;#bNn+u&DmJ=AnL%fJF}bF&3xOt z9h6B;ice@gqc~=)EU>UpQ`SkK#wb{4Hr$q$8WeJbIBOC%1XWY?rN@hRuDS@%o^2dm zU6j7($=Y9#ZSoCXIGQl685?_#ul7_AnCU|?u%U|?uF;J_&3(7>ny6k}&#WRPQM0L!zm z0|h`JH$SB`CzS!tFHX!Y0P$hQ7bGTUC#EASNKH-5$zfn&f#_!_D$N6$ivY|(CddUq PK_(_3B?n}IY$XH$iDDBH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..78f820dceffcec0714ec8cb359b6bcc9d36b0ef7 GIT binary patch literal 157 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owqF_{?HIe}CI5FP~a jff9~D`~WD_z`($K-{F7#Jg6BUJs@*|;DbGc#!dnNS9A-X literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e7ee07eb0a58c7b8e9f71eb7d29960e78a659e57 GIT binary patch literal 140 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owkF_{_I*n!jm5Sy`q W!GV#1Q3ilQX0g_;~sX$g75X01h HWN-lhN@oN9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..92ecdb18f3a26a4deef4817b4a465f3a9a4fb53e GIT binary patch literal 120 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>lQXVPIrnVggdg>OjJ{ F001O416Ke5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..05c219a845f99fdce927852ba1a16e7544f7e56a GIT binary patch literal 145 zcmb34DNPJyU}WK7;b6c7Ow4RRK`tP60OBP;?AgH3AjiPKD96yizzF2CumA}VC@ReZ Y3qW{42_OTc87Roa1f*b^KtlKc0E&DD(f|Me literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..646a5b63a29068725b8502efe470d903a7e7457d GIT binary patch literal 354 zcmb34DNPJyU}WK7;b6c7Oe}mrK`tP60OBP;?AgH3&;_J{fN_Zf0|RG6Lqlr=1H+t# z28JgowoF&P=yK_)l=aWg{$ aqZ3FiV*`T&Baj3_IYtI1n0}xVoB#kU$p^GG`V^1} zf)0!g438Wb7*94dFt9l`FtP(Ru>vs%5VHWWx1$5Y0Y(O<7@(XSP(8?OCZK*6kYON@ eo|B)Hm=j-?m{Xd{0C!+=eqM2DZYo3x4iW%qR}>5Y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f7e6369d98807863a55606fc36a411e7bf9eb4ba GIT binary patch literal 159 zcmb34DNPJyU}WK7;b6c7Ow1rrRv>nCU|^7GU|@7>aA0UWz`zj8z`z&>6qjRYU|<2# zaP38@g{7HAsSGSUKv59LOD!!aO3cwqEXZVFVTXt@6z7)~B?A?~EGyokRpT(B+jX3 MQyu)?_5F|h1dR(A=l}o! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fbc92f2a539f8d2698b38844dca6b2bacbe1c275 GIT binary patch literal 235 zcmb34DNPJyU}WK7;b6c7Oe|bLK~^AkbYNhRX<%SXfhuZXXka+lz`!tOEZg78CZCrI`UFWONtV6^b!j)5vmFjD|7M_Qy3Vbia->|7?72Wj10_d oK-vX}-53r4Enr}X1q#OjX^?dwJuK`%0tAZlON)|IA#5BZ0Hq}tqyPW_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..33ac832821e0f4e1d1449220cde309268a9f6a34 GIT binary patch literal 303 zcmb34DNPJyU}WK7;b6c7Ow61>K~^AkbYNhRX<%SXfhuZXXkeHN#PfiBIUvmhq~S&s zr52WE7Ns(<@IY1PrIwZyCFbZQ7GyFoLd8H7GZ2Gp0E#iQ18Ef?W&vUcAZ`TWSfErK zkOtWR(g(MpAh9whKQRTxFpx$zAZBD_UtJ5F_{=Rcz{$F5O*># zG#mh`XJb6T=mk{Jz|g?d>A=w7#^}J1MSz=CUDujuH F1OS|e4wwJ{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..26e83f482bd4ca7c7f69e79453f18408dbd6c313 GIT binary patch literal 144 zcmb34DNPJyU}WK7;b6c7Oiau`K@}jDV_;yEV`u;?VFZ&90>owmF&P=yK_&zN@nj&r Z0>liA4Ga#9KoSV$7#Wyg`ax2-0012f2IT+% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ee99243d5478930f4b64c42e93c52403eadd5d44 GIT binary patch literal 442 zcmZ`#L2iRE5L^gNNcx1Tc|wr#0v?b{Uw{a>5~_(K8>1Zak>;(Lu>n=By4LKhwX^Gi zx0A0wB+2qDm!rfewXci8810Hdxxp2m-EHtzxx!z^T6C*W+u4#F948zpJHg}g$3iT9K>^?T4VyN*o>mO@QgHeni_ zG?Dlk?O|2QKDBEpK`tOp0b))EhK3XeAOi>*fN(Al&jYgMfHWhJW?=`( z06}qnX;E@20~3S?CYgaGD}>JkVuRGn0V%j%Hi#rcSz=CUDwK^$fouRd38 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'