diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..11b690a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,42 @@ +name: Lint + +on: + pull_request: + branches: [ main ] + +jobs: + lint: + strategy: + fail-fast: false + matrix: + linter: [ + {"name": "tests-flake8", "format": "flake8", "cwd": ".", "cmd": "flake8 tests"}, + {"name": "tests-mypy", "format": "mypy", "cwd": ".", "cmd": "mypy tests"}, + {"name": "tests-pylint", "format": "pylint-json", "cwd": ".", "cmd": "pylint --load-plugins pylint_pytest $(Get-ChildItem -Filter *.py -Recurse tests)"}, + {"name": "eslint", "format": "eslint-unix", "cwd": "bugalua", "cmd": "node_modules\\.bin\\eslint.ps1 --format=unix ."}, + ] + name: ${{ matrix.linter.name }} + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r tests\dev-requirements.txt + python -m pip install git+https://github.com/bugale/Bugalintly.git@bugalintly + - name: Install Node.JS dependencies + run: | + cd bugalua + npm install . + - name: Lint + run: | + cd ${{ matrix.linter.cwd }} + ${{ matrix.linter.cmd }} > lint.log + $exitcode = $LASTEXITCODE + type lint.log | Lintly --log --no-request-changes --no-review-body --base-dir .. --format=${{ matrix.linter.format }} --comment-tag=${{ matrix.linter.name }} + exit $exitcode + env: + LINTLY_API_KEY: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..16c7669 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Test + +on: + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install Windows SDK 22621 + run: | + Invoke-WebRequest -UseBasicParsing -Uri 'https://go.microsoft.com/fwlink/p/?linkid=2196241' -OutFile 'winsdksetup.exe' + Start-Process -Wait '.\winsdksetup.exe' '/features OptionId.WindowsDesktopDebuggers /quiet /norestart' + Remove-Item -Force 'winsdksetup.exe' + - name: Install Bugalua + run: | + .\Install.ps1 + - name: Install test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r tests\requirements.txt + - name: Test + run: | + pytest tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a94566 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**\__pycache__ +**\package-lock.json +**\node_modules \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..72fb5b2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [], + "python.linting.mypyEnabled": true, + "python.linting.mypyArgs": ["--follow-imports=silent"], + "python.linting.pylintEnabled": true, + "python.linting.pylintArgs": ["--load-plugins", "pylint_pytest"], + "python.testing.pytestEnabled": true, + "eslint.enable": true, + "files.associations": { + "*.yaml": "home-assistant" + }, + "editor.tabSize": 4, + "[javascript]": { + "editor.tabSize": 2 + } +} \ No newline at end of file diff --git a/Install.ps1 b/Install.ps1 new file mode 100644 index 0000000..5e10d05 --- /dev/null +++ b/Install.ps1 @@ -0,0 +1,5 @@ +param ($windbgPath='C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe') +(Get-Content $PSScriptRoot\Manifest\config.xml) -Replace [regex]::escape('C:\the\path\to\this\repo'), "$PSScriptRoot" | Set-Content $PSScriptRoot\Manifest\config.xml +$TempFile = New-TemporaryFile +".settings load $PSScriptRoot\Manifest\config.xml`n.settings save`nqq`n" | Out-File -Encoding ASCII $TempFile +& "$windbgPath" "-c" "$<$TempFile" "C:\Windows\system32\cmd.exe" diff --git a/Manifest/Manifest.1.xml b/Manifest/Manifest.1.xml new file mode 100644 index 0000000..c401590 --- /dev/null +++ b/Manifest/Manifest.1.xml @@ -0,0 +1,24 @@ + + + + Bugalua + 1.0.0.0 + Lua debugging extension + + + + + + + ]]> + + + + + + + + + + + \ No newline at end of file diff --git a/Manifest/ManifestVersion.txt b/Manifest/ManifestVersion.txt new file mode 100644 index 0000000..d04da7a --- /dev/null +++ b/Manifest/ManifestVersion.txt @@ -0,0 +1,3 @@ +1 +1.0.0.0 +1 \ No newline at end of file diff --git a/Manifest/config.xml b/Manifest/config.xml new file mode 100644 index 0000000..7df73f1 --- /dev/null +++ b/Manifest/config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/bugalua/bugalua.js b/bugalua/bugalua.js new file mode 100644 index 0000000..559c51c --- /dev/null +++ b/bugalua/bugalua.js @@ -0,0 +1,194 @@ +'use strict'; + +const console = { log: (...args) => host.diagnostics.debugLog(...args, '\n') }; + +function getLuaState(luaAddr) { + // #region General Types + const ptrsize = host.currentSession.Attributes.Machine.PointerSize; + const voidType = { size: 0, name: 'void', from(addr) { return addr; } }; + + function int(size, name, signed = false) { + return { + size, + name, + from(addr) { return parseInt(host.memory.readMemoryValues(addr, 1, size, signed)[0]); }, + }; + } + const [uint8, uint32] = [int(1, 'uint8'), int(4, 'uint32')]; + const sizeT = int(ptrsize, 'size_t'); + + function ptr(type) { + return { + size: ptrsize, + name: `${type.name}*`, + from(addr) { + return { + addr: sizeT.from(addr), + type: type.name, + get value() { return type.from(sizeT.from(addr)); }, + toString() { return `[${type.name}* at 0x${addr.toString(16)}]`; }, + }; + }, + }; + } + function ptr32(type) { + return { + size: 4, + name: `${type.name}*`, + from(addr) { + return { + addr: uint32.from(addr), + type: type.name, + get value() { return type.from(uint32.from(addr)); }, + toString() { return `[${type.name}* at 0x${addr.toString(16)}]`; }, + }; + }, + }; + } + + function array(type, length) { + class Array { + constructor(addr) { + Object.defineProperties(this, { + length: { value: length, writable: false }, + [Symbol.iterator]: { + value: () => { + let index = 0; + + return { + next: () => ({ + done: index >= this.length, + value: type.from(addr + (index++) * type.size), + }), + }; + }, + }, + }); + } + } + return { size: length * type.size, name: (`${type.name}[${length}]`), from(addr) { return new Array(addr); } }; + } + + function struct(name, fieldsArr) { + let offset = 0; + const fields = []; + fieldsArr.forEach(([type, fieldName]) => { + fields.push({ type, fieldName, offset }); + offset += type.size; + }); + class Struct { + constructor(addr) { + fields.forEach((field) => { + Object.defineProperty(this, field.fieldName, { + get() { + return field.type.from(addr + field.offset); + }, + }); + }); + this.toString = () => `[struct ${name} at 0x${addr.toString(16)}]`; + } + } + return { size: offset, name, from(addr) { return new Struct(addr); } }; + } + + function defer(fn, name) { + return { get size() { return fn().size; }, name, get from() { return fn().from; } }; + } + // #endregion General Types + + // #region LuaJIT 2.0.5 Types + const MSize = uint32; + const MRef = ptr32; + const GCRef = ptr32; + + const luaJit205GCState = struct('GC_State', [ + [MSize, 'total'], + [MSize, 'threshold'], + [uint8, 'currentwhite'], + [uint8, 'state'], + [uint8, 'nocdatafin'], + [uint8, 'unused2'], + [MSize, 'sweepstr'], + [GCRef(voidType), 'root'], + [MRef(voidType), 'sweep'], + [GCRef(voidType), 'gray'], + [GCRef(voidType), 'grayagain'], + [GCRef(voidType), 'weak'], + [GCRef(voidType), 'mmudata'], + [MSize, 'stepmul'], + [MSize, 'debt'], + [MSize, 'estimate'], + [MSize, 'pause'], + ]); + + const luaJit205GlobalState = struct('global_State', [ + [ptr(GCRef(voidType)), 'strhash'], + [MSize, 'strmask'], + [MSize, 'strnum'], + [ptr(voidType), 'allocf'], + [ptr(voidType), 'allocd'], + [luaJit205GCState, 'gc'], + /* Incomplete */ + ]); + + const luaJit205LuaState = struct('lua_State', [ + [GCRef(voidType), 'nextgc'], + [uint8, 'marked'], + [uint8, 'gct'], + [uint8, 'dummy_ffid'], + [uint8, 'status'], + [MRef(luaJit205GlobalState), 'glref'], + /* Incomplete */ + ]); + // #endregion LuaJIT 2.0.5 Types + + // #region Frontend + class SizeBytes { + constructor(bytes) { + this.bytes = bytes; + } + + toString() { + if (this.bytes <= 512) { + return `${this.bytes.toLocaleString()} B`; + } + + const unitsArr = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let number = this.bytes / 1024; + let units = 0; + while (number > 512) { + number /= 1024; + units += 1; + } + return `${number.toFixed(2)} ${unitsArr[units]} (${this.bytes.toLocaleString()} B)`; + } + } + + class LuaState { + constructor(addr) { + this.L = luaJit205LuaState.from(addr); + } + + get address() { + return this.L.addr; + } + + get MemoryUsage() { + return new SizeBytes(this.L.glref.value.gc.total); + } + } + // #endregion Frontend + + return new LuaState(luaAddr); +} + +function initializeScript() { + return [ + new host.functionAlias((addr) => getLuaState(addr), 'lua'), + ]; +} + +const exports = [ + console, + initializeScript, +]; diff --git a/bugalua/package.json b/bugalua/package.json new file mode 100644 index 0000000..5cb381c --- /dev/null +++ b/bugalua/package.json @@ -0,0 +1,22 @@ +{ + "name": "bugalua", + "version": "0.0.1", + "main": "bugalua.js", + "devDependencies": { + "eslint": "*", + "eslint-config-airbnb": "*" + }, + "eslintConfig": { + "extends": [ "airbnb" ], + "rules": { + "strict": "off", + "linebreak-style": "off", + "max-classes-per-file": "off", + "no-unused-vars": ["error", { "varsIgnorePattern": "^exports$" }], + "new-cap": "off", + "no-plusplus": "off", + "radix": "off" + }, + "globals": { "host": "readonly" } + } +} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0fe1aa5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,21 @@ +[flake8] +max-line-length = 160 + +[mypy] +namespace_packages = True +strict = True +show_error_codes = True +show_column_numbers = True + +[mypy-cffi,minidump.*] +ignore_missing_imports = True + +[tool:pytest] +log_cli = 1 +log_cli_level = DEBUG +filterwarnings = error + +[pylint.config] +max-line-length = 160 +output-format = json +disable = missing-module-docstring,missing-class-docstring,missing-function-docstring,broad-except diff --git a/tests/binaries/luajit-2.0.5-Win-x64/header.h b/tests/binaries/luajit-2.0.5-Win-x64/header.h new file mode 100644 index 0000000..03cf3c9 --- /dev/null +++ b/tests/binaries/luajit-2.0.5-Win-x64/header.h @@ -0,0 +1,214 @@ +/* option for multiple returns in `lua_pcall' and `lua_call' */ +#define LUA_MULTRET -1 + + +/* +** pseudo-indices +*/ +#define LUA_REGISTRYINDEX -10000 +#define LUA_ENVIRONINDEX -10001 +#define LUA_GLOBALSINDEX -10002 + + +/* thread status; 0 is OK */ +#define LUA_YIELD 1 +#define LUA_ERRRUN 2 +#define LUA_ERRSYNTAX 3 +#define LUA_ERRMEM 4 +#define LUA_ERRERR 5 + + +typedef struct lua_State lua_State; + +typedef int (*lua_CFunction) (lua_State *L); + + +/* +** functions that read/write blocks when loading/dumping Lua chunks +*/ +typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz); + +typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud); + + +/* +** prototype for memory-allocation functions +*/ +typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize); + + +/* +** basic types +*/ +#define LUA_TNONE -1 + +#define LUA_TNIL 0 +#define LUA_TBOOLEAN 1 +#define LUA_TLIGHTUSERDATA 2 +#define LUA_TNUMBER 3 +#define LUA_TSTRING 4 +#define LUA_TTABLE 5 +#define LUA_TFUNCTION 6 +#define LUA_TUSERDATA 7 +#define LUA_TTHREAD 8 + + + +/* minimum Lua stack available to a C function */ +#define LUA_MINSTACK 20 + + +/* type of numbers in Lua */ +typedef double lua_Number; + + +/* type for integer functions */ +typedef size_t lua_Integer; + + + +/* +** state manipulation +*/ +extern lua_State *(lua_newstate) (lua_Alloc f, void *ud); +extern void (lua_close) (lua_State *L); +extern lua_State *(lua_newthread) (lua_State *L); + +extern lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf); + + +/* +** basic stack manipulation +*/ +extern int (lua_gettop) (lua_State *L); +extern void (lua_settop) (lua_State *L, int idx); +extern void (lua_pushvalue) (lua_State *L, int idx); +extern void (lua_remove) (lua_State *L, int idx); +extern void (lua_insert) (lua_State *L, int idx); +extern void (lua_replace) (lua_State *L, int idx); +extern int (lua_checkstack) (lua_State *L, int sz); + +extern void (lua_xmove) (lua_State *from, lua_State *to, int n); + + +/* +** access functions (stack -> C) +*/ + +extern int (lua_isnumber) (lua_State *L, int idx); +extern int (lua_isstring) (lua_State *L, int idx); +extern int (lua_iscfunction) (lua_State *L, int idx); +extern int (lua_isuserdata) (lua_State *L, int idx); +extern int (lua_type) (lua_State *L, int idx); +extern const char *(lua_typename) (lua_State *L, int tp); + +extern int (lua_equal) (lua_State *L, int idx1, int idx2); +extern int (lua_rawequal) (lua_State *L, int idx1, int idx2); +extern int (lua_lessthan) (lua_State *L, int idx1, int idx2); + +extern lua_Number (lua_tonumber) (lua_State *L, int idx); +extern lua_Integer (lua_tointeger) (lua_State *L, int idx); +extern int (lua_toboolean) (lua_State *L, int idx); +extern const char *(lua_tolstring) (lua_State *L, int idx, size_t *len); +extern size_t (lua_objlen) (lua_State *L, int idx); +extern lua_CFunction (lua_tocfunction) (lua_State *L, int idx); +extern void *(lua_touserdata) (lua_State *L, int idx); +extern lua_State *(lua_tothread) (lua_State *L, int idx); +extern const void *(lua_topointer) (lua_State *L, int idx); + + +/* +** push functions (C -> stack) +*/ +extern void (lua_pushnil) (lua_State *L); +extern void (lua_pushnumber) (lua_State *L, lua_Number n); +extern void (lua_pushinteger) (lua_State *L, lua_Integer n); +extern void (lua_pushlstring) (lua_State *L, const char *s, size_t l); +extern void (lua_pushstring) (lua_State *L, const char *s); +extern const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...); +extern void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); +extern void (lua_pushboolean) (lua_State *L, int b); +extern void (lua_pushlightuserdata) (lua_State *L, void *p); +extern int (lua_pushthread) (lua_State *L); + + +/* +** get functions (Lua -> stack) +*/ +extern void (lua_gettable) (lua_State *L, int idx); +extern void (lua_getfield) (lua_State *L, int idx, const char *k); +extern void (lua_rawget) (lua_State *L, int idx); +extern void (lua_rawgeti) (lua_State *L, int idx, int n); +extern void (lua_createtable) (lua_State *L, int narr, int nrec); +extern void *(lua_newuserdata) (lua_State *L, size_t sz); +extern int (lua_getmetatable) (lua_State *L, int objindex); +extern void (lua_getfenv) (lua_State *L, int idx); + + +/* +** set functions (stack -> Lua) +*/ +extern void (lua_settable) (lua_State *L, int idx); +extern void (lua_setfield) (lua_State *L, int idx, const char *k); +extern void (lua_rawset) (lua_State *L, int idx); +extern void (lua_rawseti) (lua_State *L, int idx, int n); +extern int (lua_setmetatable) (lua_State *L, int objindex); +extern int (lua_setfenv) (lua_State *L, int idx); + + +/* +** `load' and `call' functions (load and run Lua code) +*/ +extern void (lua_call) (lua_State *L, int nargs, int nresults); +extern int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc); +extern int (lua_cpcall) (lua_State *L, lua_CFunction func, void *ud); +extern int (lua_load) (lua_State *L, lua_Reader reader, void *dt, + const char *chunkname); + +extern int (lua_dump) (lua_State *L, lua_Writer writer, void *data); + + +/* +** coroutine functions +*/ +extern int (lua_yield) (lua_State *L, int nresults); +extern int (lua_resume) (lua_State *L, int narg); +extern int (lua_status) (lua_State *L); + +/* +** garbage-collection function and options +*/ + +#define LUA_GCSTOP 0 +#define LUA_GCRESTART 1 +#define LUA_GCCOLLECT 2 +#define LUA_GCCOUNT 3 +#define LUA_GCCOUNTB 4 +#define LUA_GCSTEP 5 +#define LUA_GCSETPAUSE 6 +#define LUA_GCSETSTEPMUL 7 + +extern int (lua_gc) (lua_State *L, int what, int data); + + +/* +** miscellaneous functions +*/ + +extern int (lua_error) (lua_State *L); + +extern int (lua_next) (lua_State *L, int idx); + +extern void (lua_concat) (lua_State *L, int n); + +extern lua_Alloc (lua_getallocf) (lua_State *L, void **ud); +extern void lua_setallocf (lua_State *L, lua_Alloc f, void *ud); + + +/* +** auxillary functions +*/ + +extern lua_State *(luaL_newstate) (void); +extern void luaL_openlibs(lua_State *L); +extern int luaL_loadstring(lua_State *L, const char *s); diff --git a/tests/binaries/luajit-2.0.5-Win-x64/lua51.dll b/tests/binaries/luajit-2.0.5-Win-x64/lua51.dll new file mode 100644 index 0000000..c0cde4e Binary files /dev/null and b/tests/binaries/luajit-2.0.5-Win-x64/lua51.dll differ diff --git a/tests/binaries/luajit-2.0.5-Win-x64/lua51.pdb b/tests/binaries/luajit-2.0.5-Win-x64/lua51.pdb new file mode 100644 index 0000000..bf5fc3a Binary files /dev/null and b/tests/binaries/luajit-2.0.5-Win-x64/lua51.pdb differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..779cdb3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,35 @@ +from typing import Any + +from cffi import FFI +import pytest + + +from infra import lua_loaders + + +@pytest.fixture +def _ffi_and_lua() -> tuple[FFI, Any]: + return lua_loaders.luajit_205() + + +@pytest.fixture +def ffi(_ffi_and_lua: tuple[FFI, Any]) -> FFI: + return _ffi_and_lua[0] + + +@pytest.fixture +def lua(_ffi_and_lua: tuple[FFI, Any]) -> Any: + return _ffi_and_lua[1] + + +@pytest.fixture +def lua_state(lua: Any) -> Any: + state = lua.luaL_newstate() + lua.luaL_openlibs(state) + yield state + lua.lua_close(state) + + +@pytest.fixture +def lua_state_addr(ffi: FFI, lua_state: Any) -> int: + return int(ffi.cast('size_t', lua_state)) diff --git a/tests/dev-requirements.txt b/tests/dev-requirements.txt new file mode 100644 index 0000000..fcb39b6 --- /dev/null +++ b/tests/dev-requirements.txt @@ -0,0 +1,5 @@ +pylint +pylint-pytest +mypy +flake8 +-r requirements.txt \ No newline at end of file diff --git a/tests/infra/__init__.py b/tests/infra/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/infra/dumps.py b/tests/infra/dumps.py new file mode 100644 index 0000000..f4a5eb6 --- /dev/null +++ b/tests/infra/dumps.py @@ -0,0 +1,72 @@ +import os +import tempfile +import subprocess +from typing import Any +import logging + +from cffi import FFI +from minidump.utils import createminidump + +logger = logging.getLogger('tests') + + +def take_dump(lua: Any, ffi: FFI, lua_state: Any, code: str) -> str: + """ Runs the given lua code, exposing a `dump()` global function. Returns the name of the dump file generated """ + with tempfile.NamedTemporaryFile(suffix='.dmp', delete=False) as dump_file: + filename = dump_file.name + dumps_taken = 0 + + def dump(_: Any) -> int: + nonlocal dumps_taken + logger.debug('Taking a dump') + try: + if dumps_taken == 0: + createminidump.create_dump(pid=os.getpid(), output_filename=filename, mindumptype=( + createminidump.MINIDUMP_TYPE.MiniDumpWithFullMemory | + createminidump.MINIDUMP_TYPE.MiniDumpIgnoreInaccessibleMemory | + createminidump.MINIDUMP_TYPE.MiniDumpWithUnloadedModules | + createminidump.MINIDUMP_TYPE.MiniDumpWithProcessThreadData | + createminidump.MINIDUMP_TYPE.MiniDumpWithFullMemoryInfo | + createminidump.MINIDUMP_TYPE.MiniDumpWithThreadInfo | + createminidump.MINIDUMP_TYPE.MiniDumpWithHandleData | + createminidump.MINIDUMP_TYPE.MiniDumpWithTokenInformation + )) + dumps_taken += 1 + except Exception: + logger.exception('Failed taking dump') + return 0 + + callback = ffi.callback('lua_CFunction', dump) + lua.lua_pushstring(lua_state, b'dump') + lua.lua_pushcclosure(lua_state, callback, 0) + lua.lua_settable(lua_state, lua.LUA_GLOBALSINDEX) + lua.luaL_loadstring(lua_state, code.encode('utf-8')) + if lua.lua_pcall(lua_state, 0, 0, 0) != 0: + error_message = ffi.string(lua.lua_tolstring(lua_state, -1, ffi.NULL)).decode('utf-8') + lua.lua_remove(lua_state, -1) + raise Exception(f'Error running Lua code {error_message}') + assert dumps_taken == 1, f'Expected exactly one dump to be requested, {dumps_taken} were requested' + assert os.path.isfile(filename), 'No dump was created' + logger.debug('Successfully took dump at: %s', filename) + return filename + + +def run_windbg_commands(dump: str, commands: str) -> str: + """ Opens the given dump file in WinDbg, runs the given commands, and returns the output """ + token = '***WINDBGTOKEN***' + with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', delete=False) as commands_file: + commands_file.write(f''' +!lua +.echo "{token}" +{commands} +.echo "{token}" +qq +''') + logger.debug('Running windbg commands: %s', commands) + proc = subprocess.run([r'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe', '-z', dump, '-c', f'$<{commands_file.name}'], + check=False, capture_output=True, encoding='utf-8') + # Token appears 4 times (twice inside the command echo, and twice as its output) + output = proc.stdout.split(token)[2].strip() # Take the middle part + output = '\n'.join(output.split('\n')[:-1]) # Remove the last line (which is the beginning of the token echo command) + logger.debug('windbg commands: %s returned: %s', commands, output) + return output diff --git a/tests/infra/lua_loaders.py b/tests/infra/lua_loaders.py new file mode 100644 index 0000000..f3ac2e5 --- /dev/null +++ b/tests/infra/lua_loaders.py @@ -0,0 +1,13 @@ +import os +from typing import Any + +from cffi import FFI + + +def luajit_205() -> tuple[FFI, Any]: + """ LuaJit 2.0.5 FFI object """ + ffi = FFI() + directory = os.path.join(os.path.dirname(__file__), '..', 'binaries', 'luajit-2.0.5-Win-x64') + with open(os.path.join(directory, 'header.h'), encoding='utf-8') as header: + ffi.cdef(header.read()) + return ffi, ffi.dlopen(os.path.join(directory, 'lua51.dll')) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..141ed57 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,3 @@ +pytest +cffi +minidump \ No newline at end of file diff --git a/tests/test_memory_usage.py b/tests/test_memory_usage.py new file mode 100644 index 0000000..6f284f2 --- /dev/null +++ b/tests/test_memory_usage.py @@ -0,0 +1,28 @@ +from typing import Any +import logging + +from cffi import FFI +import pytest + +from infra import dumps + +logger = logging.getLogger('tests') + + +@pytest.mark.parametrize('usage', [0, 1024, 133337, 1024 * 512, 1024 * 1024 * 512]) +def test_memory_usage(ffi: FFI, lua: Any, lua_state: Any, lua_state_addr: int, usage: int) -> None: + """ Test """ + lua.lua_newuserdata(lua_state, usage) + dump = dumps.take_dump(lua=lua, ffi=ffi, lua_state=lua_state, code=''' + gcusage = collectgarbage("count") + dump() + ''') + lua.lua_pushstring(lua_state, b'gcusage') + lua.lua_gettable(lua_state, lua.LUA_GLOBALSINDEX) + gcusage = lua.lua_tonumber(lua_state, -1) + lua.lua_remove(lua_state, -1) + assert (gcusage * 1024) >= usage, 'GC usage is smaller than expected' + + output = dumps.run_windbg_commands(dump=dump, commands=f'dx @$scriptContents.getLuaState({lua_state_addr}).MemoryUsage.bytes') + memory_used = int(output.split(':')[2].strip(), 0) + assert memory_used == gcusage * 1024, 'Memory usage is not as expected'