├── cli ├── .gitignore ├── parse.sh ├── package.json ├── tsconfig.json └── cli.ts ├── .gitignore ├── public ├── robots.txt └── manifest.json ├── src ├── setupTests.ts ├── main.tsx ├── utils.tsx ├── parser.test.ts ├── test-data.ts ├── App.test.tsx ├── index.css ├── components.test.tsx ├── __snapshots__ │ └── App.test.tsx.snap ├── App.tsx ├── analyzer.ts ├── parser.ts └── analyzer.test.ts ├── vite.config.js ├── .github └── workflows │ ├── ci.yml │ └── build-and-deploy.yml ├── SAMPLE-LOGS.md ├── tsconfig.json ├── eslint.config.js ├── LICENSE ├── README.md ├── package.json ├── index.html └── HOWTO.md /cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | build/ 3 | node_modules/ 4 | .vscode/ 5 | coverage/ 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /cli/parse.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf node_modules 3 | npm install 4 | exec ./node_modules/.bin/ts-node cli.ts "$1" 5 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpfvv-cli", 3 | "version": "0.0.1", 4 | "main": "cli.ts", 5 | "dependencies": { 6 | "ts-node": "^10.9.2", 7 | "typescript": "^5.8.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "bpfvv", 3 | "name": "BPF Verifier Visualizer", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | }, 8 | "include": ["cli.ts", "../src/parser.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById("root") as HTMLElement, 8 | ); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig(() => { 5 | return { 6 | build: { 7 | outDir: 'build', 8 | }, 9 | plugins: [react()], 10 | base: '/bpfvv/', 11 | server: { 12 | fs: { 13 | strict: false, // allow url content loading 14 | } 15 | }, 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | - run: npm install 17 | - run: npm run lint 18 | - run: npm run test 19 | - run: npm run typecheck 20 | - run: npm run build 21 | -------------------------------------------------------------------------------- /SAMPLE-LOGS.md: -------------------------------------------------------------------------------- 1 | # Sample Logs 2 | 3 | - [log1](https://gist.githubusercontent.com/jordalgo/f59c216ba91e269632cc9fdc6fc67be3/raw/d058a99a48450131015608195cc5e33364312492/gistfile1.txt) 4 | - [log2](https://gist.githubusercontent.com/jordalgo/061d908e411e3477848be5c36c99e596/raw/bb2a35f7d5288472801211f73c95ad6e71c6b808/gistfile1.txt) 5 | - [log3](https://gist.githubusercontent.com/theihor/1bea72b50f6834c00b67a3087304260e/raw/9c0cb831a4924e5f0f63cc1e0d9620aec771d31f/pyperf600-v1.log) 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "esModuleInterop": true, 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /cli/cli.ts: -------------------------------------------------------------------------------- 1 | import * as parser from '../src/parser'; 2 | import * as fs from 'node:fs'; 3 | 4 | function cli_main() { 5 | const args = process.argv.slice(2); 6 | if (args.length === 0) { 7 | console.error('Usage: ts-node parser.ts '); 8 | process.exit(1); 9 | } 10 | const filePath = args[0]; 11 | try { 12 | const fileContents = fs.readFileSync(filePath, 'utf8'); 13 | const lines = fileContents.split('\n'); 14 | const parsed = lines.map(parser.parseLine); 15 | console.log(JSON.stringify(parsed, null, 2)); 16 | process.exit(0); 17 | } catch (error) { 18 | console.error(`Error reading file: ${error instanceof Error ? error.message : String(error)}`); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | cli_main(); 24 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | - run: npm install 14 | - run: npm run test 15 | - run: npm run build 16 | - uses: actions/upload-pages-artifact@v3 17 | with: 18 | path: build/ 19 | 20 | deploy: 21 | needs: build 22 | permissions: 23 | contents: read 24 | pages: write 25 | id-token: write 26 | runs-on: ubuntu-latest 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | steps: 31 | - name: Deploy to GitHub Pages 32 | id: deployment 33 | uses: actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import tseslint from '@typescript-eslint/eslint-plugin'; 3 | import tsparser from '@typescript-eslint/parser'; 4 | 5 | export default [ 6 | js.configs.recommended, 7 | { 8 | files: ['**/*.ts', '**/*.tsx'], 9 | languageOptions: { 10 | parser: tsparser, 11 | parserOptions: { 12 | ecmaVersion: 'latest', 13 | sourceType: 'module', 14 | }, 15 | globals: { 16 | console: 'readonly', 17 | }, 18 | }, 19 | plugins: { 20 | '@typescript-eslint': tseslint, 21 | }, 22 | rules: { 23 | ...tseslint.configs.recommended.rules, 24 | 'no-undef': 'off', 25 | 'no-case-declarations': 'off', 26 | }, 27 | }, 28 | { 29 | files: ['cli/**/*.ts'], 30 | languageOptions: { 31 | parser: tsparser, 32 | parserOptions: { 33 | ecmaVersion: 'latest', 34 | sourceType: 'module', 35 | }, 36 | globals: { 37 | console: 'readonly', 38 | process: 'readonly', 39 | Buffer: 'readonly', 40 | }, 41 | }, 42 | plugins: { 43 | '@typescript-eslint': tseslint, 44 | }, 45 | rules: { 46 | ...tseslint.configs.recommended.rules, 47 | }, 48 | }, 49 | { 50 | ignores: ['node_modules/**', 'dist/**', 'coverage/**', 'src/__snapshots__/**'], 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2025 Meta Platforms, Inc. and affiliates. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/libbpf/bpfvv/actions/workflows/ci.yml/badge.svg)](https://github.com/libbpf/bpfvv/actions/workflows/ci.yml) 2 | 3 | **bpfvv** stands for BPF Verifier Visualizer 4 | 5 | https://libbpf.github.io/bpfvv/ 6 | 7 | BPF Verifier Visualizer is a tool to analyze Linux Kernel BPF verifier logs. 8 | 9 | The goal of bpfvv is to help BPF programmers debug verification failures. 10 | 11 | The user can load a text file, and the app will attempt to parse it as a verifier log. Successfully parsed lines produce a state which is then visualized in the UI. You can think of this as a primitive debugger UI, except it interprets a log and not a runtime state of a program. 12 | 13 | For more information on how to use **bpfvv** see the [HOWTO.md](https://github.com/libbpf/bpfvv/blob/master/HOWTO.md) 14 | 15 | ## Development 16 | 17 | - Fork the website repo: https://github.com/libbpf/bpfvv.git 18 | 19 | - Clone your fork: 20 | - `git clone https://github.com//bpfvv.git` 21 | 22 | - Setup node modules: 23 | - `npm install` 24 | 25 | - Develop the app: 26 | - `npm start` 27 | 28 | - Build the app for static testing: 29 | - `npm run build` 30 | 31 | - Serve the statically built app: 32 | - `npm run serve` 33 | 34 | - Format your code: 35 | - `npm run format` 36 | 37 | - To run lint, typecheck, and tests: 38 | - `npm run check` 39 | 40 | - If everything is OK, push your branch, create a PR. 41 | 42 | --- 43 | 44 | This is a self-contained web app that runs entirely on the client side. There is no backend server. Once loaded, it operates within the browser. 45 | 46 | * To learn more about BPF visit https://ebpf.io/ 47 | * See also: https://github.com/eddyz87/log2dot 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpfvv", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "localdata": "^0.1.4", 7 | "react": "^19.1.0", 8 | "react-dom": "^19.1.0", 9 | "react-window": "^2.1.1" 10 | }, 11 | "scripts": { 12 | "start": "vite", 13 | "build": "vite build", 14 | "serve": "vite preview", 15 | "test": "jest", 16 | "format": "prettier src/* --write", 17 | "lint": "eslint", 18 | "typecheck": "tsc --noEmit", 19 | "check": "npm run lint && npm run typecheck && npm run test" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "type": "module", 28 | "jest": { 29 | "transform": { 30 | "^.+\\.(ts|tsx)$": "ts-jest" 31 | } 32 | }, 33 | "homepage": "https://libbpf.github.io/bpfvv/", 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@testing-library/jest-dom": "^6.6.3", 48 | "@testing-library/react": "^16.3.0", 49 | "@types/jest": "^30.0.0", 50 | "@types/react": "^19.1.8", 51 | "@types/react-dom": "^19.1.6", 52 | "@typescript-eslint/eslint-plugin": "^8.37.0", 53 | "@typescript-eslint/parser": "^8.37.0", 54 | "@vitejs/plugin-react": "^4.7.0", 55 | "jest": "^30.0.4", 56 | "jest-environment-jsdom": "^30.0.4", 57 | "prettier": "^3.6.2", 58 | "ts-jest": "^29.4.0", 59 | "typescript": "^5.8.3", 60 | "vite": "^7.0.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | BPF Verifier Visualizer 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | import { BpfState } from "./analyzer"; 2 | import { CSourceRow } from "./components"; 3 | import { 4 | BpfInstruction, 5 | BpfInstructionKind, 6 | BpfJmpKind, 7 | ParsedLine, 8 | ParsedLineType, 9 | parseStackSlotId, 10 | StackSlotId, 11 | } from "./parser"; 12 | 13 | export async function fetchLogFromUrl(url: string) { 14 | try { 15 | const response = await fetch(url); 16 | if (!response.ok) { 17 | return null; 18 | } 19 | return await response.text(); 20 | } catch (error) { 21 | console.error("Error fetching log:", error); 22 | return null; 23 | } 24 | } 25 | 26 | export function normalIdx(idx: number, linesLen: number): number { 27 | return Math.min(Math.max(0, idx), linesLen - 1); 28 | } 29 | 30 | export function siblingInsLine( 31 | insLines: ParsedLine[], 32 | idx: number, 33 | delta: number, 34 | ): number { 35 | // if delta is 1 we are looking for the next instruction 36 | // if delta is -1 we are looking for the previous instruction 37 | const n = insLines.length; 38 | for (let i = normalIdx(idx + delta, n); 0 <= i && i < n; i += delta) { 39 | const line = insLines[i]; 40 | if (line.type === ParsedLineType.INSTRUCTION) { 41 | return i; 42 | } 43 | } 44 | return normalIdx(idx, n); 45 | } 46 | 47 | export function siblingCLine( 48 | cLines: CSourceRow[], 49 | idx: number, 50 | delta: number, 51 | ): string { 52 | let cLineId = ""; 53 | let nextVisibleIdx = idx; 54 | while (true) { 55 | nextVisibleIdx += delta; 56 | const cLine = cLines[nextVisibleIdx]; 57 | if (cLine.type === "c_line" && !cLine.ignore) { 58 | cLineId = cLine.sourceId; 59 | break; 60 | } 61 | if (nextVisibleIdx <= 0 || nextVisibleIdx >= cLines.length - 1) { 62 | break; 63 | } 64 | } 65 | 66 | return cLineId; 67 | } 68 | 69 | export function insEntersNewFrame(ins: BpfInstruction): boolean { 70 | if (ins.kind === BpfInstructionKind.JMP) { 71 | switch (ins.jmpKind) { 72 | case BpfJmpKind.SUBPROGRAM_CALL: 73 | return true; 74 | case BpfJmpKind.HELPER_CALL: 75 | if (ins.target.startsWith("bpf_loop#")) return true; 76 | } 77 | } 78 | return false; 79 | } 80 | 81 | export function foreachStackSlot( 82 | currentFrame: number, 83 | func: (id: string) => void, 84 | ) { 85 | // current stack (no frame qualifier) 86 | for (let i = 0; i <= 512; i += 1) { 87 | const id = `fp-${i}`; 88 | func(id); 89 | } 90 | 91 | // nested stacks, e.g. fp[1]-16 92 | for (let frame = currentFrame - 1; frame >= 0; frame--) { 93 | for (let i = 0; i <= 512; i += 1) { 94 | const id = `fp[${frame}]-${i}`; 95 | func(id); 96 | } 97 | } 98 | } 99 | 100 | function normalMemSlotId( 101 | stackSlotId: StackSlotId, 102 | offset: number, 103 | frame: number, 104 | ): string { 105 | if (stackSlotId.frame !== undefined) { 106 | return `fp[${stackSlotId.frame}]${offset}`; 107 | } else { 108 | return `fp[${frame}]${offset}`; 109 | } 110 | } 111 | 112 | export function stackSlotIdFromDisplayId( 113 | displayId: string, 114 | frame: number, 115 | ): string { 116 | const stackSlotId = parseStackSlotId(displayId); 117 | if (stackSlotId) { 118 | return normalMemSlotId(stackSlotId, stackSlotId.offset, frame); 119 | } 120 | return displayId; 121 | } 122 | 123 | export function stackSlotIdForIndirectAccess( 124 | bpfState: BpfState, 125 | srcMemRef: { reg: string; offset: number } | undefined, 126 | ): string | null { 127 | if (!srcMemRef) { 128 | return null; 129 | } 130 | const regValue = bpfState.values.get(srcMemRef.reg); 131 | const stackSlotId = regValue ? parseStackSlotId(regValue.value) : null; 132 | if (stackSlotId === null) { 133 | return null; 134 | } 135 | const totalOffset = stackSlotId.offset + srcMemRef.offset; 136 | return normalMemSlotId(stackSlotId, totalOffset, bpfState.frame); 137 | } 138 | 139 | /* This class is essentially a string map, except it normalizes stack slot access ids 140 | * to a canonical form of `fp[${frame}]${offset}` 141 | * If [] is absent, the key refers to the current frame. 142 | */ 143 | export class BpfMemSlotMap extends Map { 144 | currentFrame: number; 145 | constructor(currentFrame: number) { 146 | super(); 147 | this.currentFrame = currentFrame; 148 | } 149 | 150 | setFrame(frame: number): void { 151 | this.currentFrame = frame; 152 | } 153 | 154 | get(key: string): T | undefined { 155 | return super.get(stackSlotIdFromDisplayId(key, this.currentFrame)); 156 | } 157 | 158 | set(key: string, value: T): this { 159 | return super.set(stackSlotIdFromDisplayId(key, this.currentFrame), value); 160 | } 161 | 162 | has(key: string): boolean { 163 | return super.has(stackSlotIdFromDisplayId(key, this.currentFrame)); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseLine, 3 | parseBpfStateExprs, 4 | ParsedLineType, 5 | BpfJmpKind, 6 | BpfJmpCode, 7 | BpfInstruction, 8 | BpfAluInstruction, 9 | BpfInstructionKind, 10 | ParsedLine, 11 | BpfJmpInstruction, 12 | BpfTargetJmpInstruction, 13 | BpfAddressSpaceCastInstruction, 14 | InstructionLine, 15 | KnownMessageInfoType, 16 | BpfInstructionClass, 17 | OpcodeSource, 18 | } from "./parser"; 19 | 20 | const AluInstructionSample = "0: (b7) r2 = 1 ; R2_w=1"; 21 | const BPFStateExprSample = "R2_w=1 R10=fp0 fp-24_w=1"; 22 | const MemoryWriteSample = "1: (7b) *(u64 *)(r10 -24) = r2" + BPFStateExprSample; 23 | const CallInstructionSample = "7: (85) call bpf_probe_read_user#112"; 24 | const AddrSpaceCastSample1 = 25 | "2976: (bf) r1 = addr_space_cast(r7, 0, 1) ; frame1: R1_w=arena"; 26 | const AddrSpaceCastSample2 = "75: (bf) r1 = addr_space_cast(r2, 0, 64)"; 27 | const ConditionalPseudoMayGotoSample = "2984: (e5) may_goto pc+3"; 28 | const ConditionalPseudoGotoOrNopSample = "2984: (e5) goto_or_nop pc+3"; 29 | const CSourceLineSample = "; n->key = 3; @ rbtree.c:201"; 30 | const CSourceLineEmptySample1 = "; @ foo.h:42"; 31 | const CSourceLineEmptySample2 = "; int i = 0; @ foo.h:0"; 32 | 33 | function expectBpfIns(line: ParsedLine): BpfInstruction { 34 | expect(line.type).toBe(ParsedLineType.INSTRUCTION); 35 | const insLine = line; 36 | return insLine.bpfIns; 37 | } 38 | 39 | function expectBpfAluIns(line: ParsedLine): BpfAluInstruction { 40 | const ins = expectBpfIns(line); 41 | expect(ins.kind).toBe(BpfInstructionKind.ALU); 42 | return ins; 43 | } 44 | 45 | function expectBpfJmpIns(line: ParsedLine): BpfJmpInstruction { 46 | const ins = expectBpfIns(line); 47 | expect(ins.kind).toBe(BpfInstructionKind.JMP); 48 | return ins; 49 | } 50 | 51 | function expectBpfMayJmpInstruction(line: ParsedLine): BpfTargetJmpInstruction { 52 | const ins = expectBpfIns(line); 53 | expect(ins.kind).toBe(BpfInstructionKind.JMP); 54 | const jmpIns = ins; 55 | expect(jmpIns.jmpKind).toBe(BpfJmpKind.MAY_GOTO); 56 | return ins; 57 | } 58 | 59 | function expectBpfJmpOrNopInstruction( 60 | line: ParsedLine, 61 | ): BpfTargetJmpInstruction { 62 | const ins = expectBpfIns(line); 63 | expect(ins.kind).toBe(BpfInstructionKind.JMP); 64 | const jmpIns = ins; 65 | expect(jmpIns.jmpKind).toBe(BpfJmpKind.GOTO_OR_NOP); 66 | return ins; 67 | } 68 | 69 | function expectAddrSpaceCastIns( 70 | line: ParsedLine, 71 | ): BpfAddressSpaceCastInstruction { 72 | const ins = expectBpfIns(line); 73 | expect(ins.kind).toBe(BpfInstructionKind.ADDR_SPACE_CAST); 74 | return ins; 75 | } 76 | 77 | describe("parser", () => { 78 | it("parses ALU instructions with state expressions", () => { 79 | const parsed = parseLine(AluInstructionSample, 0); 80 | const ins: BpfAluInstruction = expectBpfAluIns(parsed); 81 | const bpfStateExprs = (parsed).bpfStateExprs; 82 | expect(ins?.pc).toBe(0); 83 | expect(ins?.operator).toBe("="); 84 | expect(ins?.writes).toContain("r2"); 85 | expect(bpfStateExprs[0]).toMatchObject({ 86 | id: "r2", 87 | value: "1", 88 | }); 89 | }); 90 | 91 | it("parses memory write instruction", () => { 92 | const parsed = parseLine(MemoryWriteSample, 0); 93 | const ins: BpfAluInstruction = expectBpfAluIns(parsed); 94 | const bpfStateExprs = (parsed).bpfStateExprs; 95 | expect(ins.pc).toBe(1); 96 | expect(ins.kind).toBe(BpfInstructionKind.ALU); 97 | expect(ins.dst.id).toBe("fp-24"); 98 | expect(ins.src.id).toBe("r2"); 99 | expect(bpfStateExprs.length).toBe(3); 100 | }); 101 | 102 | it("parses call instruction", () => { 103 | const parsed = parseLine(CallInstructionSample, 7); 104 | let ins = expectBpfJmpIns(parsed); 105 | expect(ins.jmpKind).toBe(BpfJmpKind.HELPER_CALL); 106 | const targetIns = ins; 107 | expect(targetIns.target).toBe("bpf_probe_read_user#112"); 108 | expect(ins.reads).toContain("r1"); 109 | expect(ins.writes).toContain("r0"); 110 | }); 111 | 112 | it("parses verifier state expressions", () => { 113 | const { exprs, rest } = parseBpfStateExprs(BPFStateExprSample); 114 | expect(rest).toBe(""); 115 | expect(exprs.map((e) => e.id)).toEqual(["r2", "r10", "fp-24"]); 116 | }); 117 | 118 | it("parses addr_space_cast", () => { 119 | let parsed = parseLine(AddrSpaceCastSample1, 13); 120 | let ins = expectAddrSpaceCastIns(parsed); 121 | expect(ins.dst.id).toBe("r1"); 122 | expect(ins.src.id).toBe("r7"); 123 | expect(ins.directionStr).toBe("0, 1"); 124 | expect(ins.reads).toContain("r7"); 125 | expect(ins.writes).toContain("r1"); 126 | 127 | parsed = parseLine(AddrSpaceCastSample2, 13); 128 | ins = expectAddrSpaceCastIns(parsed); 129 | expect(ins.dst.id).toBe("r1"); 130 | expect(ins.src.id).toBe("r2"); 131 | expect(ins.directionStr).toBe("0, 64"); 132 | expect(ins.reads).toContain("r2"); 133 | expect(ins.writes).toContain("r1"); 134 | }); 135 | 136 | it("parses conditional pseudo may goto", () => { 137 | const parsed = parseLine(ConditionalPseudoMayGotoSample, 0); 138 | const ins = expectBpfMayJmpInstruction(parsed); 139 | expect(ins.target).toBe("pc+3"); 140 | expect(ins.opcode.code).toBe(BpfJmpCode.JCOND); 141 | }); 142 | 143 | it("parses conditional pseudo goto_or_nop", () => { 144 | const parsed = parseLine(ConditionalPseudoGotoOrNopSample, 0); 145 | const ins = expectBpfJmpOrNopInstruction(parsed); 146 | expect(ins.target).toBe("pc+3"); 147 | expect(ins.opcode.code).toBe(BpfJmpCode.JCOND); 148 | }); 149 | 150 | it("parses C source line matching RE_C_SOURCE_LINE regex", () => { 151 | const parsed = parseLine(CSourceLineSample, 5); 152 | expect(parsed).toEqual({ 153 | type: ParsedLineType.C_SOURCE, 154 | idx: 5, 155 | raw: CSourceLineSample, 156 | content: "n->key = 3;", 157 | fileName: "rbtree.c", 158 | lineNum: 201, 159 | id: "rbtree.c:201", 160 | ignore: false, 161 | }); 162 | }); 163 | 164 | it("marks empty source lines", () => { 165 | expect(parseLine(CSourceLineEmptySample1, 13)).toEqual({ 166 | type: ParsedLineType.C_SOURCE, 167 | idx: 13, 168 | raw: CSourceLineEmptySample1, 169 | content: "", 170 | fileName: "foo.h", 171 | lineNum: 42, 172 | id: "foo.h:42", 173 | ignore: true, 174 | }); 175 | expect(parseLine(CSourceLineEmptySample2, 31)).toEqual({ 176 | type: ParsedLineType.C_SOURCE, 177 | idx: 31, 178 | raw: CSourceLineEmptySample2, 179 | content: "int i = 0;", 180 | fileName: "foo.h", 181 | lineNum: 0, 182 | id: "foo.h:0", 183 | ignore: true, 184 | }); 185 | }); 186 | 187 | describe("Known message parsing", () => { 188 | const GlobalFuncValidSample = 189 | "Func#123 ('my_func') is global and assumed valid."; 190 | const NotAKnownMessage = "Some other verifier message that doesn't match"; 191 | 192 | it("parses global function valid messages", () => { 193 | const parsed = parseLine(GlobalFuncValidSample, 10); 194 | expect(parsed).toMatchObject({ 195 | type: ParsedLineType.KNOWN_MESSAGE, 196 | idx: 10, 197 | raw: GlobalFuncValidSample, 198 | info: { 199 | type: KnownMessageInfoType.GLOBAL_FUNC_VALID, 200 | funcId: 123, 201 | funcName: "my_func", 202 | }, 203 | }); 204 | }); 205 | 206 | it("does not parse non-matching lines as known messages", () => { 207 | const parsed = parseLine(NotAKnownMessage, 5); 208 | expect(parsed.type).toBe(ParsedLineType.UNRECOGNIZED); 209 | }); 210 | }); 211 | 212 | describe("parses exprs with pc", () => { 213 | const str = 214 | "64: (55) if r0 != 0x0 goto pc+6 71: R0=ptr_node_data(non_own_ref,off=16) R7=2 R10=fp0"; 215 | it("parses global function valid messages", () => { 216 | const parsed = parseLine(str, 10); 217 | expect(parsed).toMatchObject({ 218 | type: ParsedLineType.INSTRUCTION, 219 | idx: 10, 220 | raw: str, 221 | bpfIns: { 222 | pc: 64, 223 | opcode: { 224 | code: BpfJmpCode.JSET, 225 | iclass: BpfInstructionClass.JMP, 226 | source: OpcodeSource.K, 227 | }, 228 | cond: { left: { id: "r0" }, right: { id: "IMM" } }, 229 | }, 230 | }); 231 | const insLine = parsed; 232 | 233 | expect(insLine.bpfStateExprs.length).toBe(3); 234 | expect(insLine.bpfStateExprs[0]).toEqual({ 235 | id: "r0", 236 | value: "ptr_node_data(non_own_ref,off=16)", 237 | rawKey: "R0", 238 | frame: 0, 239 | pc: 71, 240 | }); 241 | expect(insLine.bpfStateExprs[1]).toEqual({ 242 | id: "r7", 243 | value: "2", 244 | rawKey: "R7", 245 | frame: 0, 246 | pc: 71, 247 | }); 248 | expect(insLine.bpfStateExprs[2]).toEqual({ 249 | id: "r10", 250 | value: "fp-0", 251 | rawKey: "R10", 252 | frame: 0, 253 | pc: 71, 254 | }); 255 | }); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /src/test-data.ts: -------------------------------------------------------------------------------- 1 | export const SAMPLE_LOG_DATA_1 = ` 2 | 0: (18) r1 = 0x11 ; R1_w=17 3 | ; if (!n) @ rbtree.c:199 4 | 2: (b7) r2 = 0 ; R2_w=0 5 | 3: (85) call bpf_obj_new_impl#54651 ; R0_w=ptr_or_null_node_data(id=2,ref_obj_id=2) refs=2 6 | 4: (bf) r6 = r0 ; R0_w=ptr_or_null_node_data(id=2,ref_obj_id=2) R6_w=ptr_or_null_node_data(id=2,ref_obj_id=2) refs=2 7 | 5: (b7) r7 = 1 ; R7_w=1 refs=2 8 | ; if (!n) @ rbtree.c:199 9 | 6: (15) if r6 == 0x0 goto pc+104 ; R6_w=ptr_node_data(ref_obj_id=2) refs=2 10 | 7: (b7) r1 = 4 ; R1_w=4 ref 11 | `; 12 | 13 | export const SAMPLE_LOG_DATA_2 = `PROCESSING rbtree.bpf.o/rbtree_first_and_remove, DURATION US: 842, VERDICT: failure, VERIFIER LOG: 14 | arg#0 reference type('UNKNOWN ') size cannot be determined: -22 15 | 0: R1=ctx() R10=fp0 16 | ; n = bpf_obj_new(typeof(*n)); @ rbtree.c:198 17 | 0: (18) r1 = 0x11 ; R1_w=17 18 | 2: (b7) r2 = 0 ; R2_w=0 19 | 3: (85) call bpf_obj_new_impl#54651 ; R0_w=ptr_or_null_node_data(id=2,ref_obj_id=2) refs=2 20 | 4: (bf) r6 = r0 ; R0_w=ptr_or_null_node_data(id=2,ref_obj_id=2) R6_w=ptr_or_null_node_data(id=2,ref_obj_id=2) refs=2 21 | 5: (b7) r7 = 1 ; R7_w=1 refs=2 22 | ; if (!n) @ rbtree.c:199 23 | 6: (15) if r6 == 0x0 goto pc+104 ; R6_w=ptr_node_data(ref_obj_id=2) refs=2 24 | 7: (b7) r1 = 4 ; R1_w=4 refs=2 25 | ; n->data = 4; @ rbtree.c:202 26 | 8: (7b) *(u64 *)(r6 +8) = r1 ; R1_w=4 R6_w=ptr_node_data(ref_obj_id=2) refs=2 27 | 9: (b7) r1 = 3 ; R1_w=3 refs=2 28 | ; n->key = 3; @ rbtree.c:201 29 | 10: (7b) *(u64 *)(r6 +0) = r1 ; R1_w=3 R6_w=ptr_node_data(ref_obj_id=2) refs=2 30 | ; m = bpf_obj_new(typeof(*m)); @ rbtree.c:204 31 | 11: (18) r1 = 0x11 ; R1_w=17 refs=2 32 | 13: (b7) r2 = 0 ; R2_w=0 refs=2 33 | 14: (85) call bpf_obj_new_impl#54651 ; R0=ptr_or_null_node_data(id=4,ref_obj_id=4) refs=2,4 34 | 15: (bf) r8 = r0 ; R0=ptr_or_null_node_data(id=4,ref_obj_id=4) R8_w=ptr_or_null_node_data(id=4,ref_obj_id=4) refs=2,4 35 | ; if (!m) @ rbtree.c:205 36 | 16: (15) if r8 == 0x0 goto pc+52 ; R8_w=ptr_node_data(ref_obj_id=4) refs=2,4 37 | 17: (b7) r1 = 6 ; R1_w=6 refs=2,4 38 | ; m->data = 6; @ rbtree.c:208 39 | 18: (7b) *(u64 *)(r8 +8) = r1 ; R1_w=6 R8_w=ptr_node_data(ref_obj_id=4) refs=2,4 40 | 19: (b7) r1 = 5 ; R1_w=5 refs=2,4 41 | ; m->key = 5; @ rbtree.c:207 42 | 20: (7b) *(u64 *)(r8 +0) = r1 ; R1_w=5 R8_w=ptr_node_data(ref_obj_id=4) refs=2,4 43 | ; o = bpf_obj_new(typeof(*o)); @ rbtree.c:210 44 | 21: (18) r1 = 0x11 ; R1_w=17 refs=2,4 45 | 23: (b7) r2 = 0 ; R2_w=0 refs=2,4 46 | 24: (85) call bpf_obj_new_impl#54651 ; R0=ptr_or_null_node_data(id=6,ref_obj_id=6) refs=2,4,6 47 | ; if (!o) @ rbtree.c:211 48 | 25: (15) if r0 == 0x0 goto pc+79 ; R0=ptr_node_data(ref_obj_id=6) refs=2,4,6 49 | 26: (b7) r7 = 2 ; R7_w=2 refs=2,4,6 50 | ; o->data = 2; @ rbtree.c:214 51 | 27: (7b) *(u64 *)(r0 +8) = r7 ; R0=ptr_node_data(ref_obj_id=6) R7_w=2 refs=2,4,6 52 | 28: (b7) r1 = 1 ; R1_w=1 refs=2,4,6 53 | ; o->key = 1; @ rbtree.c:213 54 | 29: (7b) *(u64 *)(r0 +0) = r1 ; R0=ptr_node_data(ref_obj_id=6) R1_w=1 refs=2,4,6 55 | ; bpf_spin_lock(&glock); @ rbtree.c:216 56 | 30: (18) r1 = 0xff434b28008e3de8 ; R1_w=map_value(map=.data.A,ks=4,vs=72,off=16) refs=2,4,6 57 | 32: (bf) r9 = r0 ; R0=ptr_node_data(ref_obj_id=6) R9_w=ptr_node_data(ref_obj_id=6) refs=2,4,6 58 | 33: (85) call bpf_spin_lock#93 ; refs=2,4,6 59 | ; bpf_rbtree_add(&groot, &n->node, less); @ rbtree.c:217 60 | 34: (bf) r2 = r6 ; R2_w=ptr_node_data(ref_obj_id=2) R6=ptr_node_data(ref_obj_id=2) refs=2,4,6 61 | 35: (07) r2 += 16 ; R2_w=ptr_node_data(ref_obj_id=2,off=16) refs=2,4,6 62 | 36: (18) r1 = 0xff434b28008e3dd8 ; R1_w=map_value(map=.data.A,ks=4,vs=72) refs=2,4,6 63 | 38: (18) r3 = 0x53 ; R3_w=func() refs=2,4,6 64 | 40: (b7) r4 = 0 ; R4_w=0 refs=2,4,6 65 | 41: (b7) r5 = 0 ; R5=0 refs=2,4,6 66 | 42: (85) call bpf_rbtree_add_impl#54894 ; R0_w=scalar() R6=ptr_node_data(non_own_ref) R7=2 R8=ptr_node_data(ref_obj_id=4) R9=ptr_node_data(ref_obj_id=6) R10=fp0 refs=4,6 67 | ; bpf_rbtree_add(&groot, &m->node, less); @ rbtree.c:218 68 | 43: (07) r8 += 16 ; R8_w=ptr_node_data(ref_obj_id=4,off=16) refs=4,6 69 | 44: (18) r1 = 0xff434b28008e3dd8 ; R1_w=map_value(map=.data.A,ks=4,vs=72) refs=4,6 70 | 46: (bf) r2 = r8 ; R2_w=ptr_node_data(ref_obj_id=4,off=16) R8_w=ptr_node_data(ref_obj_id=4,off=16) refs=4,6 71 | 47: (18) r3 = 0x4a ; R3_w=func() refs=4,6 72 | 49: (b7) r4 = 0 ; R4_w=0 refs=4,6 73 | 50: (b7) r5 = 0 ; R5=0 refs=4,6 74 | 51: (85) call bpf_rbtree_add_impl#54894 ; R0_w=scalar() R6=ptr_node_data(non_own_ref) R7=2 R8=ptr_node_data(non_own_ref,off=16) R9=ptr_node_data(ref_obj_id=6) R10=fp0 refs=6 75 | ; bpf_rbtree_add(&groot, &o->node, less); @ rbtree.c:219 76 | 52: (07) r9 += 16 ; R9_w=ptr_node_data(ref_obj_id=6,off=16) refs=6 77 | 53: (18) r1 = 0xff434b28008e3dd8 ; R1_w=map_value(map=.data.A,ks=4,vs=72) refs=6 78 | 55: (bf) r2 = r9 ; R2_w=ptr_node_data(ref_obj_id=6,off=16) R9_w=ptr_node_data(ref_obj_id=6,off=16) refs=6 79 | 56: (18) r3 = 0x41 ; R3_w=func() refs=6 80 | 58: (b7) r4 = 0 ; R4_w=0 refs=6 81 | 59: (b7) r5 = 0 ; R5=0 refs=6 82 | 60: (85) call bpf_rbtree_add_impl#54894 ; R0_w=scalar() R6=ptr_node_data(non_own_ref) R7=2 R8=ptr_node_data(non_own_ref,off=16) R9=ptr_node_data(non_own_ref,off=16) R10=fp0 83 | ; res = bpf_rbtree_first(&groot); @ rbtree.c:221 84 | 61: (18) r1 = 0xff434b28008e3dd8 ; R1_w=map_value(map=.data.A,ks=4,vs=72) 85 | 63: (85) call bpf_rbtree_first#54897 ; R0_w=ptr_or_null_node_data(id=7,non_own_ref,off=16) 86 | ; if (!res) { @ rbtree.c:222 87 | 64: (55) if r0 != 0x0 goto pc+6 71: R0=ptr_node_data(non_own_ref,off=16) R6=ptr_node_data(non_own_ref) R7=2 R8=ptr_node_data(non_own_ref,off=16) R9=ptr_node_data(non_own_ref,off=16) R10=fp0 88 | ; first_data[0] = o->data; @ rbtree.c:228 89 | 71: (79) r1 = *(u64 *)(r0 -8) ; R0=ptr_node_data(non_own_ref,off=16) R1_w=scalar() 90 | 72: (18) r2 = 0xff6f3b1a00e97010 ; R2_w=map_value(map=rbtree.data,ks=4,vs=32,off=16) 91 | 74: (7b) *(u64 *)(r2 +0) = r1 ; R1_w=scalar() R2_w=map_value(map=rbtree.data,ks=4,vs=32,off=16) 92 | ; res = bpf_rbtree_remove(&groot, &o->node); @ rbtree.c:230 93 | 75: (18) r1 = 0xff434b28008e3dd8 ; R1_w=map_value(map=.data.A,ks=4,vs=72) 94 | 77: (bf) r2 = r0 ; R0=ptr_node_data(non_own_ref,off=16) R2_w=ptr_node_data(non_own_ref,off=16) 95 | 78: (85) call bpf_rbtree_remove#54900 ; R0_w=ptr_or_null_node_data(id=9,ref_obj_id=9,off=16) 96 | 79: (bf) r8 = r0 ; R0_w=ptr_or_null_node_data(id=9,ref_obj_id=9,off=16) R8_w=ptr_or_null_node_data(id=9,ref_obj_id=9,off=16) 97 | ; bpf_spin_unlock(&glock); @ rbtree.c:231 98 | 80: (18) r1 = 0xff434b28008e3de8 ; R1_w=map_value(map=.data.A,ks=4,vs=72,off=16) 99 | 82: (85) call bpf_spin_unlock#94 ; refs=9 100 | 83: (b7) r7 = 5 ; R7_w=5 refs=9 101 | ; if (!res) @ rbtree.c:233 102 | 84: (15) if r8 == 0x0 goto pc+26 ; R8=ptr_node_data(ref_obj_id=9,off=16) refs=9 103 | ; removed_key = o->key; @ rbtree.c:237 104 | 85: (79) r1 = *(u64 *)(r8 -16) ; R1_w=scalar() R8=ptr_node_data(ref_obj_id=9,off=16) refs=9 105 | 86: (18) r2 = 0xff6f3b1a00e97008 ; R2_w=map_value(map=rbtree.data,ks=4,vs=32,off=8) refs=9 106 | 88: (7b) *(u64 *)(r2 +0) = r1 ; R1_w=scalar() R2_w=map_value(map=rbtree.data,ks=4,vs=32,off=8) refs=9 107 | ; o = container_of(res, struct node_data, node); @ rbtree.c:236 108 | 89: (07) r8 += -16 ; R8_w=ptr_node_data(ref_obj_id=9) refs=9 109 | ; bpf_obj_drop(o); @ rbtree.c:238 110 | 90: (bf) r1 = r8 ; R1_w=ptr_node_data(ref_obj_id=9) R8_w=ptr_node_data(ref_obj_id=9) refs=9 111 | 91: (b7) r2 = 0 ; R2_w=0 refs=9 112 | 92: (85) call bpf_obj_drop_impl#54635 ; 113 | ; bpf_spin_lock(&glock); @ rbtree.c:240 114 | 93: (18) r1 = 0xff434b28008e3de8 ; R1_w=map_value(map=.data.A,ks=4,vs=72,off=16) 115 | 95: (85) call bpf_spin_lock#93 ; 116 | ; res = bpf_rbtree_first(&groot); @ rbtree.c:241 117 | 96: (18) r1 = 0xff434b28008e3dd8 ; R1_w=map_value(map=.data.A,ks=4,vs=72) 118 | 98: (85) call bpf_rbtree_first#54897 ; R0_w=ptr_or_null_node_data(id=10,non_own_ref,off=16) 119 | ; if (!res) { @ rbtree.c:242 120 | 99: (55) if r0 != 0x0 goto pc+13 113: R0_w=ptr_node_data(non_own_ref,off=16) R6=scalar() R7=5 R8=scalar() R9=scalar() R10=fp0 121 | ; first_data[1] = o->data; @ rbtree.c:248 122 | 113: (79) r1 = *(u64 *)(r0 -8) ; R0_w=ptr_node_data(non_own_ref,off=16) R1_w=scalar() 123 | 114: (18) r2 = 0xff6f3b1a00e97010 ; R2_w=map_value(map=rbtree.data,ks=4,vs=32,off=16) 124 | 116: (7b) *(u64 *)(r2 +8) = r1 ; R1_w=scalar() R2_w=map_value(map=rbtree.data,ks=4,vs=32,off=16) 125 | ; bpf_spin_unlock(&glock); @ rbtree.c:249 126 | 117: (18) r1 = 0xff434b28008e3de8 ; R1_w=map_value(map=.data.A,ks=4,vs=72,off=16) 127 | 119: (85) call bpf_spin_unlock#94 ; 128 | ; return n->data; @ rbtree.c:251 129 | 120: (79) r7 = *(u64 *)(r6 +8) 130 | R6 invalid mem access 'scalar' 131 | verification time 842 usec 132 | stack depth 0+0 133 | processed 94 insns (limit 1000000) max_states_per_insn 0 total_states 10 peak_states 10 mark_read 6 134 | `; 135 | 136 | export const SAMPLE_LOG_DATA_ERORR = `120: (79) r7 = *(u64 *)(r6 +8) 137 | R6 invalid mem access 'scalar' 138 | verification time 842 usec 139 | `; 140 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import "@testing-library/jest-dom"; 5 | import { render, createEvent, fireEvent } from "@testing-library/react"; 6 | import ldb from "localdata"; 7 | import App from "./App"; 8 | import { 9 | SAMPLE_LOG_DATA_1, 10 | SAMPLE_LOG_DATA_2, 11 | SAMPLE_LOG_DATA_ERORR, 12 | } from "./test-data"; 13 | 14 | // Mock the localdata module 15 | jest.mock("localdata", () => ({ 16 | __esModule: true, 17 | default: { 18 | get: jest.fn((_, callback) => { 19 | callback(""); 20 | }), 21 | set: jest.fn(), 22 | delete: jest.fn(), 23 | list: jest.fn(), 24 | getAll: jest.fn(), 25 | clear: jest.fn(), 26 | }, 27 | })); 28 | 29 | // use screen.debug(); to log the whole DOM 30 | 31 | const DOM_EL_FAIL = "DOM Element missing"; 32 | 33 | // Mock Date to return a consistent timestamp for snapshot tests 34 | const MOCK_DATE = new Date("2025-01-15T12:00:00.000Z"); 35 | 36 | describe("App", () => { 37 | beforeEach(() => { 38 | // Clear all mocks before each test 39 | jest.clearAllMocks(); 40 | 41 | // Reset default mock implementation for ldb.get to return null (no stored logs) 42 | (ldb.get as jest.Mock).mockImplementation((_, callback) => { 43 | callback(null); 44 | }); 45 | 46 | // Mock Date to return consistent timestamp 47 | jest.spyOn(global, "Date").mockImplementation(() => { 48 | const mockDate = MOCK_DATE; 49 | mockDate.toLocaleTimeString = jest.fn().mockReturnValue("12:00:00 PM"); 50 | return mockDate; 51 | }); 52 | 53 | const mockObserverInstance = { 54 | observe: jest.fn(), 55 | unobserve: jest.fn(), 56 | disconnect: jest.fn(), 57 | }; 58 | global.ResizeObserver = jest.fn(() => mockObserverInstance); 59 | }); 60 | 61 | afterEach(() => { 62 | // Restore Date mock 63 | jest.restoreAllMocks(); 64 | }); 65 | 66 | it("renders the correct starting elements", () => { 67 | const { container } = render(); 68 | expect(container).toMatchSnapshot(); 69 | }); 70 | 71 | it("renders the log visualizer when text is pasted", async () => { 72 | const { container, rerender } = render(); 73 | 74 | const inputEl = document.getElementById("input-text"); 75 | if (!inputEl) { 76 | throw new Error(DOM_EL_FAIL); 77 | } 78 | 79 | fireEvent( 80 | inputEl, 81 | createEvent.paste(inputEl, { 82 | clipboardData: { 83 | getData: () => 84 | "314: (73) *(u8 *)(r7 +1303) = r1 ; frame1: R1_w=0 R7=map_value(off=0,ks=4,vs=2808,imm=0)", 85 | }, 86 | }), 87 | ); 88 | 89 | rerender(); 90 | expect(container).toMatchSnapshot(); 91 | 92 | expect(document.getElementById("c-source-container")).toBeTruthy(); 93 | expect(document.getElementById("log-content")).toBeTruthy(); 94 | expect(document.getElementById("state-panel")).toBeTruthy(); 95 | 96 | // Hit the clear button and make sure we go back to the intial state 97 | const clearEl = document.getElementById("clear"); 98 | if (!clearEl) { 99 | throw new Error(DOM_EL_FAIL); 100 | } 101 | fireEvent(clearEl, createEvent.click(clearEl)); 102 | 103 | rerender(); 104 | expect(container).toMatchSnapshot(); 105 | 106 | expect(document.getElementById("c-source-container")).toBeFalsy(); 107 | expect(document.getElementById("log-content")).toBeFalsy(); 108 | expect(document.getElementById("state-panel")).toBeFalsy(); 109 | }); 110 | 111 | it("jumps to the next/prev instruction on key up/down", async () => { 112 | render(); 113 | 114 | const inputEl = document.getElementById("input-text"); 115 | if (!inputEl) { 116 | throw new Error(DOM_EL_FAIL); 117 | } 118 | 119 | fireEvent( 120 | inputEl, 121 | createEvent.paste(inputEl, { 122 | clipboardData: { 123 | getData: () => SAMPLE_LOG_DATA_1, 124 | }, 125 | }), 126 | ); 127 | 128 | // Need to show all log lines 129 | const checkboxEl = document.getElementById("csource-toggle"); 130 | if (!checkboxEl) { 131 | throw new Error(DOM_EL_FAIL); 132 | } 133 | fireEvent(checkboxEl, createEvent.click(checkboxEl)); 134 | 135 | const logContainerEl = document.getElementById("log-container"); 136 | 137 | const line1 = document.getElementById("line-1"); 138 | const line2 = document.getElementById("line-2"); 139 | const line3 = document.getElementById("line-3"); 140 | 141 | if (!line1 || !line3 || !line2 || !logContainerEl) { 142 | throw new Error(DOM_EL_FAIL); 143 | } 144 | 145 | expect(line1.innerHTML).toBe( 146 | '
0
r1 = 0x11
', 147 | ); 148 | 149 | // Show that the next line is not an instruction 150 | expect(line2.innerHTML).toBe( 151 | '
\n
; if (!n) @ rbtree.c:199
', 152 | ); 153 | 154 | // Show the one after IS an instruction 155 | expect(line3.innerHTML).toBe( 156 | '
2
r2 = 0
', 157 | ); 158 | 159 | // Click on Line 1 160 | fireEvent(line1, createEvent.click(line1)); 161 | expect(line1.classList.contains("selected-line")).toBeTruthy(); 162 | 163 | // Keyboard Down Arrow 164 | fireEvent.keyDown(logContainerEl, { key: "ArrowDown", code: "ArrowDown" }); 165 | expect(line1.classList.contains("selected-line")).toBeFalsy(); 166 | expect(line2.classList.contains("selected-line")).toBeFalsy(); 167 | expect(line3.classList.contains("selected-line")).toBeTruthy(); 168 | 169 | // Keyboard Up Arrow 170 | fireEvent.keyDown(logContainerEl, { key: "ArrowUp", code: "ArrowUp" }); 171 | expect(line1.classList.contains("selected-line")).toBeTruthy(); 172 | expect(line2.classList.contains("selected-line")).toBeFalsy(); 173 | expect(line3.classList.contains("selected-line")).toBeFalsy(); 174 | }); 175 | 176 | it("c lines and state panel containers are collapsible", async () => { 177 | render(); 178 | 179 | const inputEl = document.getElementById("input-text"); 180 | if (!inputEl) { 181 | throw new Error("Input text is missing"); 182 | } 183 | 184 | fireEvent( 185 | inputEl, 186 | createEvent.paste(inputEl, { 187 | clipboardData: { 188 | getData: () => SAMPLE_LOG_DATA_1, 189 | }, 190 | }), 191 | ); 192 | 193 | const cSourceEl = document.getElementById("c-source-container"); 194 | const cSourceContent = document.getElementById("c-source-content"); 195 | 196 | expect(cSourceContent).toBeVisible(); 197 | 198 | const cSourceHideShow = cSourceEl?.querySelector(".hide-show-button"); 199 | 200 | if (!cSourceHideShow) { 201 | throw new Error(DOM_EL_FAIL); 202 | } 203 | 204 | fireEvent(cSourceHideShow, createEvent.click(cSourceHideShow)); 205 | expect(cSourceContent).not.toBeVisible(); 206 | 207 | const statePanelEl = document.getElementById("state-panel"); 208 | const statePanelHeader = document.getElementById("state-panel-header"); 209 | 210 | expect(statePanelHeader).toBeVisible(); 211 | 212 | const statePanelHideShow = statePanelEl?.querySelector(".hide-show-button"); 213 | 214 | if (!statePanelHideShow) { 215 | throw new Error(DOM_EL_FAIL); 216 | } 217 | 218 | fireEvent(statePanelHideShow, createEvent.click(statePanelHideShow)); 219 | expect(statePanelHeader).not.toBeVisible(); 220 | }); 221 | 222 | it("highlights the associated c source or log line(s) when the other is clicked ", async () => { 223 | // Note: I haven't figured out how to get react-window to scroll in jest dom tests 224 | // so just make the list height static so it renders the lines we care about 225 | render(); 226 | 227 | const inputEl = document.getElementById("input-text"); 228 | if (!inputEl) { 229 | throw new Error(DOM_EL_FAIL); 230 | } 231 | 232 | fireEvent( 233 | inputEl, 234 | createEvent.paste(inputEl, { 235 | clipboardData: { 236 | getData: () => SAMPLE_LOG_DATA_2, 237 | }, 238 | }), 239 | ); 240 | 241 | const line4El = document.getElementById("line-4"); 242 | const cLineEl = document.getElementById("line-rbtree.c:198"); 243 | if (!line4El || !cLineEl) { 244 | throw new Error(DOM_EL_FAIL); 245 | } 246 | 247 | expect(line4El.classList).not.toContain("selected-line"); 248 | expect(cLineEl.classList).not.toContain("selected-line"); 249 | 250 | // Click on the first instruction log line 251 | fireEvent(line4El, createEvent.click(line4El)); 252 | 253 | expect(line4El.classList).toContain("selected-line"); 254 | expect(cLineEl.classList).toContain("selected-line"); 255 | 256 | // Click on another log line 257 | const line10El = document.getElementById("line-10"); 258 | if (!line10El) { 259 | throw new Error(DOM_EL_FAIL); 260 | } 261 | 262 | fireEvent(line10El, createEvent.click(line10El)); 263 | 264 | expect(line4El.classList).not.toContain("selected-line"); 265 | expect(cLineEl.classList).not.toContain("selected-line"); 266 | 267 | // Click on the first c source line 268 | fireEvent(cLineEl, createEvent.click(cLineEl)); 269 | 270 | expect(line4El.classList).toContain("selected-line"); 271 | expect(cLineEl.classList).toContain("selected-line"); 272 | 273 | // The other instructions for this source line should also be selected 274 | const followingIns = ["line-5", "line-6", "line-7", "line-8"]; 275 | followingIns.forEach((lineId) => { 276 | const el = document.getElementById(lineId); 277 | if (!el) { 278 | throw new Error(DOM_EL_FAIL); 279 | } 280 | expect(el.classList).toContain("selected-line"); 281 | }); 282 | }); 283 | 284 | it("labels the final error message", async () => { 285 | render(); 286 | 287 | const inputEl = document.getElementById("input-text"); 288 | if (!inputEl) { 289 | throw new Error(DOM_EL_FAIL); 290 | } 291 | 292 | fireEvent( 293 | inputEl, 294 | createEvent.paste(inputEl, { 295 | clipboardData: { 296 | getData: () => SAMPLE_LOG_DATA_ERORR, 297 | }, 298 | }), 299 | ); 300 | 301 | const line1El = document.getElementById("line-1"); 302 | if (!line1El) { 303 | throw new Error(DOM_EL_FAIL); 304 | } 305 | 306 | expect(line1El.classList).toContain("error-message"); 307 | expect(line1El.innerHTML).toBe( 308 | '
\n
R6 invalid mem access \'scalar\'
', 309 | ); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /HOWTO.md: -------------------------------------------------------------------------------- 1 | ### Disclaimer 2 | 3 | Like many other debugging tools, **bpfvv** may help you better understand **what** is happening with the verification of your BPF program but it is up to you to figure out **why** it is happening. 4 | 5 | # How to use bpfvv 6 | 7 | The tool itself is hosted here: https://libbpf.github.io/bpfvv/ 8 | 9 | You can load a log by pasting it into the text box or choosing a local file. 10 | 11 | You can also use the `url` query parameter to link to a raw log file, for example: 12 | ``` 13 | https://libbpf.github.io/bpfvv/?url=https://gist.githubusercontent.com/theihor/e0002c119414e6b40e2192bd7ced01b1/raw/866bcc155c2ce848dcd4bc7fd043a97f39a2d370/gistfile1.txt 14 | ``` 15 | 16 | The app expects BPF verifier log of `BPF_LOG_LEVEL1`[^1]. This is a log 17 | that you get when your BPF program has failed verification on load 18 | attempt. 19 | 20 | Here is a small example: 21 | ``` 22 | processed 23 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1 23 | ERROR: Error loading BPF program for usdt___a_out_test_struct_by_val_reg_pair_loc0_2. 24 | Kernel error log: 25 | 0: R1=ctx() R10=fp0 26 | ; @ bpftrace.bpf.o:0 27 | 0: (b7) r2 = 1 ; R2_w=1 28 | 1: (7b) *(u64 *)(r10 -24) = r2 ; R2_w=1 R10=fp0 fp-24_w=1 29 | 2: (79) r3 = *(u64 *)(r1 +32) ; R1=ctx() R3_w=scalar() 30 | 3: (07) r3 += -16 ; R3_w=scalar() 31 | 4: (bf) r1 = r10 ; R1_w=fp0 R10=fp0 32 | 5: (07) r1 += -8 ; R1_w=fp-8 33 | 6: (b7) r2 = 16 ; R2_w=16 34 | 7: (85) call bpf_probe_read_user#112 35 | invalid indirect access to stack R1 off=-8 size=16 36 | processed 8 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0 37 | ERROR: Loading BPF object(s) failed. 38 | ``` 39 | 40 | This log represents a particular trace through the BPF program that 41 | led to an invalid state (as judged by the BPF verifier). It contains a 42 | lot of information about the interpreted state of the program on each 43 | instruction. The app parses the log and re-constructs program states 44 | in order to display potentially useful information in interactive way. 45 | 46 | ## UI overview 47 | 48 | There are three main views of the program: 49 | * (on the left) C source view 50 | * (in the middle) interactive instruction stream 51 | * (on the right) program state: known values of registers and stack slots for the *selected log line* 52 | 53 | The left and right views are collapsible 54 | 55 | https://github.com/user-attachments/assets/758d650b-22f1-49f0-ab46-ae1a089667a8 56 | 57 | ### Top bar 58 | 59 | The top bar contains basic app controls such as: 60 | * clear current log 61 | * load an example log 62 | * load a local file 63 | * link to this howto doc 64 | 65 | https://github.com/user-attachments/assets/4d3f8aa0-cb9d-46e0-ae46-a1224c7a5600 66 | 67 | ### The instruction stream 68 | 69 | The main view of the log is the interactive instruction stream. 70 | 71 | Notice that the displayed text has content different from the raw log. 72 | For example, consider this line: 73 | ``` 74 | 1: (7b) *(u64 *)(r10 -24) = r2 ; R2_w=1 R10=fp0 fp-24_w=1 75 | ``` 76 | In the log view you will only see: 77 | ``` 78 | *(u64 *)(r10 -24) = r2 79 | ``` 80 | And program counter (pc) in a spearate column on the left. 81 | 82 | This is intentional, as the comment on the right in the original line 83 | is the state of values as reported by BPF verifier. Since it is 84 | captured and displayed in a separate view, printing it in the log view 85 | is redundant. 86 | 87 | Some instructions also are printed differently to facilitate 88 | interactive features. Notable example is call instructions. 89 | 90 | For example, consider the following raw log line: 91 | ``` 92 | 7: (85) call bpf_probe_read_user#112 93 | ``` 94 | 95 | It is displayed like this: 96 | ``` 97 | r0 = bpf_probe_read_user(dst: r1, size: r2, unsafe_ptr: r3) 98 | ``` 99 | 100 | If bpfvv is aware of a helper signature, it knows the number and names of arguments and displays them in the format `name: reg`. 101 | For known helpers its name is also a link to documentation for that helper. 102 | 103 | Notice also that the lines not recognized by the parser are greyed 104 | out. If you notice an unrecognized instruction, please submit a bug 105 | report. 106 | 107 | #### Data dependencies 108 | 109 | The app computes a use-def analysis [^2] and you can interactively view dependencies between the instructions. 110 | 111 | The concept is simple. Every instruction may read some slots (registers, stack, memory) and write to others. 112 | Knowing these it is possible to determine, for a given slot, where its value came from, from what slot, and at what instruction. 113 | 114 | You can view the results of this analysis by clicking on some instruction operands (registers and stack slots). 115 | 116 | The selected slot is identified by a box around it. This selection changes the log view, greying out "irrelevant" instructions, and leaving only data-dependent instructions in the foreground. 117 | 118 | On the left side of the instruction stream are the lines visualizing the dependencies. The lines are interactive and can be used for navigation. 119 | 120 | https://github.com/user-attachments/assets/82ae80d6-314e-47bf-9892-f5dded4b9944 121 | 122 | #### Subprogram calls 123 | 124 | When there is a subprogram call in the log instruction stream, the 125 | stack frames are tracked by the app when computing state. When a subprogram 126 | call is detected it is visualized in the main log view. 127 | 128 | https://github.com/user-attachments/assets/14b2302e-9814-4d9a-ae94-e176727fd11a 129 | 130 | ### The state panel 131 | 132 | The state panel displays the current state of the program based on the loaded log, with the current state determined by the line selected in the instruction stream view. 133 | 134 | Remember that the verifier log is a trace through the program. 135 | This means that a particular instruction may be visited more than once, and the state at the same instruction (but a different point of execution) is usually also different. And so a log line roughly represents a particular point of the program execution, as interpreted by the BPF verifier. 136 | 137 | The verifier reports changes in the program state like this: 138 | ``` 139 | 1: (7b) *(u64 *)(r10 -24) = r2 ; R2_w=1 R10=fp0 fp-24_w=1 140 | ``` 141 | After the semicolon `;`, there are expressions showing relevant register and stack slot states. The visualizer accumulates this information from all the prior instructions, and in the state panel this accumulated state is displayed. 142 | 143 | The header of the state panel shows the context of the state: log line number, C line number, program counter (PC) and the stack frame index. 144 | 145 | The known values of the registers and stack slots are displayed in a table. 146 | 147 | The background color of a row in the state panel indicates that the relevant value has been affected by the selected instruction. 148 | Rows marked with red background indicate a "write" and the previous value is also often displayed, for example: 149 | ``` 150 | r6 scalar(id=1) -> 0 151 | ``` 152 | This means that current instruction changes the value of `r6` from `scalar(id=1)` to `0`. 153 | 154 | The values that are read by the current instruction have a blue background. 155 | 156 | Note that for "update" instructions (such as `r1 += 8`), the slot will be marked as written. 157 | 158 | This then allows you to "step through" the instruction stream and watch how the values are changing, similar to classic debugger UIs. 159 | You can click on the lines that interest you, or use arrow keys to navigate. 160 | 161 | https://github.com/user-attachments/assets/c6b5b5b1-30fb-4309-a90a-1832a0a33502 162 | 163 | #### The rows in the state panel are clickable! 164 | 165 | It is sometimes useful to jump to the source of a particular slot value from the selected instruction, even if the slot is not relevant to that instruction. 166 | 167 | https://github.com/user-attachments/assets/8f5d03cc-54a5-426b-8428-c8b11f4ccf11 168 | 169 | ### The C source view 170 | 171 | The C source view panel (on the left) shows reconstructed C source lines. 172 | 173 | A raw verifier log might contain source line information, and bpfvv attempts to reconstruct the source code and associate it with the instructions. 174 | Here is how it looks in the raw log: 175 | ``` 176 | 1800: R1=scalar() R10=fp0 177 | ; int rb_print(rbtree_t __arg_arena *rbtree) @ rbtree.bpf.c:507 178 | 1800: (b4) w0 = -22 ; R0_w=0xffffffea 179 | ; if (unlikely(!rbtree)) @ rbtree.bpf.c:517 180 | 1801: (15) if r1 == 0x0 goto pc+132 ; R1=scalar(umin=1) 181 | ``` 182 | 183 | The original source code is not available in the log of course. So bpfvv doesn't have enough information to even format it properly. 184 | 185 | However, it allows you to see a rough correspondence between BPF instructions and the original C source code. 186 | 187 | Be aware though that this information is noisy and may be inaccurate, since it reached the visualizer through a long way: 188 | * the compiler generated DWARF with line info, which is already "best-effort" 189 | * DWARF was transformed into BTF with line data 190 | * BTF was processed by the verifier and available information was dumped interleaved with the program trace 191 | 192 | https://github.com/user-attachments/assets/3e8c52f0-3823-4d5f-abbd-f7c2d8e31d19 193 | 194 | ### The bottom panel 195 | 196 | The bottom panel shows original log text for the selected line and for the current hovered line. 197 | It is sometimes useful to check the source of the information displayed by the visualizer. 198 | 199 | 200 | ## Not frequently asked questions 201 | 202 | ### What exactly do "read" and "written" values means here? 203 | 204 | Here is a couple of trivial examples: 205 | * `r1 = 0` this is a write to `r1` 206 | * `r2 = r3` this is a read of `r3` and write to `r2` 207 | * `r2 += 1` this is a read of `r2` and write to `r2`, aka an update 208 | 209 | Here is a couple of more complicated examples: 210 | * `*(u64 *)(r10 -32) = r1` this is a read of `r1` and a write to `fp-32` 211 | * `r10` is effectively constant[^3], as it is always a pointer to the top of a BPF stack frame, so stores to `r10-offset` are writes to the stack slots, identified by `fp-off` or `fp[frame]-off` in the visualizer 212 | * `r1 = *(u64 *)(r2 -8)` this is a write to `r1` and a read of `r2`, however it may also be a read of the stack, if `r2` happens to contain a pointer to the stack slot 213 | 214 | Most instructions have intrinsic "read" and "write" sets, defined by its semantics. However context also matters, as you can see from the last example. 215 | 216 | The visualizer takes into account a few important things, when determining data dependencies: 217 | * it is aware of scratch and callee-saved register semantics of subprogram/helper calls 218 | * it is aware of the stack frames: we enter new stack memory in a subprogram, and pop back on exit 219 | * it is aware of indirect stack slot access and basic pointer arithmetic 220 | 221 | ### Side effects? 222 | 223 | One counterintuitive thing about data dependencies in the context of BPF verification is that the instructions which don't do any arithmetic or memory stores can still change the progam state. 224 | 225 | Remember, we are looking at the BPF verifier log. 226 | The BPF verifier simulates the execution of a program, which requires maintaining a virtual state of the program. 227 | This means that whenever the verifier gains some knowledge about a value (which is not necesarily an intrinsic write instruction), it will update the program state. 228 | 229 | For example, when processing conditional jumps such as `if (r2 == 0) goto pc+6`, 230 | the verifier usually explores both branches. But in both cases it gained information 231 | about `r2`: it's either 0 or not. And so while there was no explicit write into r2, 232 | it's value is known (and has changed) after the jump instruction, when you look at 233 | it in the verifier log. 234 | 235 | https://github.com/user-attachments/assets/94d271e2-f033-439b-8554-d9f8a66b4143 236 | 237 | ### What if we write to memory or a BPF arena? 238 | 239 | Currently non-stack memory access is a "black hole" from the point of 240 | view of use-def analysis in this app. The reason is that it's 241 | impossible to be sure what is the value of a memory slot between 242 | stores and loads from it, because it may have been written outside of 243 | BPF program, and because it's not always simple to identify a specific 244 | memory slot. 245 | 246 | So, when you see a store/load instruction to/from something like 247 | `*(u32 *)(r8 +0)` you can only click on r8 to check it's 248 | dependencies. If you see `*(u32 *)(r8 +0)` down the instruction 249 | stream, even if value of r8 hasn't changed, the analysis does not 250 | recognize these slots as "the same". 251 | 252 | **Unless** `r8` contains a pointer to a stack slot. 253 | In that case you can click both on the register to see where its value came from, and on the dereference expression to see where the stack slot value came from. 254 | 255 | https://github.com/user-attachments/assets/f345ec63-b91d-411c-b1d2-3890ed8f1c99 256 | 257 | ### An instruction is highlighted as dependency, but I don't understand why. Is that a bug? 258 | 259 | Probably not[^4]. 260 | 261 | The visualizer has a single source of information: the verifier log. 262 | The log contains two streams of information: the instructions and the associated state change, as reported by the verifier. 263 | 264 | Some of the state that the visualizer computes is derived from the instructions themselves. 265 | However, the state reported by the verifier always takes precedence. 266 | 267 | Since the values in the context of the visualizer are just strings, if the verifier reported a slightly different string, we treat it as an update. 268 | For example, you might see something like this: 269 | ``` 270 | r8 ptr_or_null_node_data(id=9,ref_obj_id=9,off=16) -> ptr_node_data(ref_obj_id=9,off=16) 271 | ``` 272 | 273 | The verifier reported a different value, and that's what bpfvv shows. 274 | 275 | ## Footnotes 276 | 277 | [^1]: `BPF_LOG_LEVEL2` can be parsed, however since level 2 log contains 278 | all states of the BPF program explored by the verifier, and the app does 279 | not distinguish between them (yet), the accumulated state at a particular 280 | log line is likely to be wrong. Also, log level 2 is usually quite big, so 281 | the browser will not be happy to render it. 282 | 283 | [^2]: https://en.wikipedia.org/wiki/Use-define_chain 284 | 285 | [^3]: https://docs.cilium.io/en/latest/reference-guides/bpf/architecture/ 286 | 287 | [^4]: But maybe yes... If you suspect a bug, please report. 288 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --yellow: rgba(255, 246, 133, 1); 3 | --yellow-70: rgba(255, 246, 133, 0.7); 4 | --yellow-50: rgba(255, 246, 133, 0.5); 5 | --primary-dark: rgba(4, 4, 4, 1); 6 | --mid-gray: rgb(212, 212, 220); 7 | --mid-gray-50: rgba(212, 212, 220, 0.5); 8 | --mid-gray-20: rgba(245, 245, 245, 1); 9 | --dark-blue: rgb(0, 73, 183); 10 | --light-blue: rgb(0, 221, 255); 11 | --light-blue-20: rgba(0, 221, 255, 0.2); 12 | --bright-red: rgb(255, 29, 88); 13 | --bright-red-20: rgba(255, 29, 88, 0.2); 14 | } 15 | 16 | body { 17 | margin: 0; 18 | padding: 0px; 19 | font-family: "Roboto Mono", monospace; 20 | font-size: 14px; 21 | } 22 | 23 | a { 24 | color: var(--dark-blue); 25 | } 26 | 27 | h1 { 28 | font-size: 20px; 29 | margin: 0px; 30 | padding: 0px 50px 0px 0px; 31 | font-family: "Roboto", sans-serif; 32 | color: var(--primary-dark); 33 | } 34 | 35 | .container { 36 | display: flex; 37 | flex-direction: column; 38 | gap: 8px; 39 | max-width: none; 40 | margin: 0; 41 | height: calc(100vh); /* Account for body padding */ 42 | } 43 | 44 | .main-content { 45 | display: flex; 46 | min-height: 0; /* Important for flex child to respect parent height */ 47 | font-size: 14px; 48 | border: 2px solid var(--mid-gray); 49 | border-radius: 4px; 50 | margin: 10px 16px 0px 16px; 51 | height: calc( 52 | 100% - 136px 53 | ); /* This number is the height of the nav bar and the bottom hint container */ 54 | } 55 | 56 | #input-text { 57 | height: 100%; 58 | padding: 10px; 59 | border: 2px dashed var(--primary-dark); 60 | font-size: 18px; 61 | margin: 10px 16px 8px 16px; 62 | font-family: "Roboto", sans-serif; 63 | } 64 | 65 | .nav-arrow { 66 | position: absolute; 67 | font-size: 30px; 68 | line-height: 32px; 69 | font-weight: normal; 70 | top: 0px; 71 | text-align: center; 72 | cursor: pointer; 73 | right: 0px; 74 | width: 40px; 75 | height: 40px; 76 | background-color: var(--mid-gray-20); 77 | font-family: "Roboto Mono", monospace; 78 | z-index: 2; 79 | } 80 | 81 | .nav-arrow:hover { 82 | color: var(--dark-blue); 83 | background-color: var(--mid-gray); 84 | } 85 | 86 | .nav-arrow .nav-arrow-tooltip { 87 | display: none; 88 | } 89 | 90 | .nav-arrow:hover .nav-arrow-tooltip { 91 | display: block; 92 | } 93 | 94 | .nav-arrow-tooltip { 95 | position: absolute; 96 | top: 6px; 97 | width: 160px; 98 | font-size: 12px; 99 | background-color: #f8f8f8; 100 | padding: 5px; 101 | border: 1px solid var(--mid-gray); 102 | color: #000; 103 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 104 | right: 30px; 105 | line-height: 20px; 106 | z-index: 1; 107 | } 108 | 109 | /* Top Navigation */ 110 | 111 | .navigation-panel { 112 | display: flex; 113 | align-items: center; 114 | padding: 10px 16px 0px 16px; 115 | font-family: "Roboto", sans-serif; 116 | } 117 | 118 | .line-nav-item { 119 | height: 26px; 120 | display: inline-block; 121 | border-left: 2px solid var(--mid-gray); 122 | padding: 6px 20px 0px; 123 | width: fit-content; 124 | } 125 | 126 | .nav-button { 127 | background-color: transparent; 128 | display: inline-block; 129 | color: var(--primary-dark); 130 | font-family: "Roboto", sans-serif; 131 | border: 0px; 132 | text-decoration: underline; 133 | } 134 | 135 | .nav-button:hover { 136 | color: var(--dark-blue); 137 | } 138 | 139 | .file-input-container { 140 | display: flex; 141 | justify-content: left; 142 | align-items: center; 143 | } 144 | 145 | #file-input { 146 | min-width: 200px; 147 | } 148 | 149 | #goto-line-input { 150 | width: 60px; 151 | } 152 | 153 | #log-example-dropdown { 154 | margin: 0px 10px; 155 | } 156 | 157 | .howto-container { 158 | flex-grow: 1; 159 | text-align: right; 160 | } 161 | 162 | .howto-link { 163 | font-weight: bold; 164 | } 165 | 166 | /* C Source Lines */ 167 | 168 | .c-source-panel { 169 | position: relative; 170 | border-right: 2px solid var(--mid-gray); 171 | } 172 | 173 | .c-source-file { 174 | width: 100%; 175 | } 176 | 177 | .filename-header { 178 | font-weight: bold; 179 | padding: 10px 0px 10px 10px; 180 | border-top: 2px solid var(--mid-gray); 181 | border-bottom: 2px solid var(--mid-gray); 182 | font-family: "Roboto", sans-serif; 183 | box-sizing: border-box; 184 | position: relative; 185 | display: inline-block; 186 | z-index: 1; 187 | cursor: pointer; 188 | } 189 | 190 | .filename-tooltip { 191 | background-color: #f8f8f8; 192 | padding: 5px 8px; 193 | border: 1px solid var(--mid-gray); 194 | color: #000; 195 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 196 | line-height: 20px; 197 | position: absolute; 198 | z-index: 1; 199 | visibility: hidden; 200 | top: 38px; 201 | left: 1%; 202 | font-weight: normal; 203 | } 204 | 205 | .filename-header:hover .filename-tooltip { 206 | visibility: visible; 207 | } 208 | 209 | .c-source-file:first-child .filename-header { 210 | border-top: 0px; 211 | } 212 | 213 | .file-lines { 214 | display: flex; 215 | flex-direction: row; 216 | flex: 1; 217 | min-width: 0; 218 | gap: 0; 219 | overflow: auto; 220 | } 221 | 222 | .c-source-line { 223 | color: var(--dark-blue); 224 | white-space: pre; 225 | } 226 | 227 | /* Log Lines */ 228 | 229 | #log-container, 230 | #c-source-container { 231 | flex: 1; 232 | min-width: 0; 233 | gap: 0; 234 | flex-grow: 1; 235 | position: relative; 236 | } 237 | 238 | #log-content, 239 | #c-source-content { 240 | height: 100%; 241 | } 242 | 243 | .log-nav-button { 244 | line-height: 38px; 245 | box-sizing: border-box; 246 | } 247 | 248 | #goto-start { 249 | right: 40px; 250 | width: 42px; 251 | padding-right: 4px; 252 | border-right: 2px solid var(--mid-gray); 253 | } 254 | 255 | #goto-end { 256 | padding-left: 6px; 257 | } 258 | 259 | #goto-start .log-button-txt { 260 | transform: rotate(-90deg); 261 | } 262 | 263 | #goto-end .log-button-txt { 264 | transform: rotate(90deg); 265 | } 266 | 267 | .pc-number, 268 | .line-number { 269 | color: var(--primary-dark); 270 | background-color: var(--mid-gray-20); 271 | border-right: 2px solid var(--mid-gray); 272 | min-width: 50px; 273 | line-height: 1.4; 274 | text-align: right; 275 | white-space: pre; 276 | margin-right: 8px; 277 | padding-right: 8px; 278 | } 279 | 280 | .selected-line .pc-number { 281 | font-weight: bold; 282 | } 283 | 284 | .source-lines { 285 | background: white; 286 | line-height: 1.4; 287 | white-space: pre; 288 | height: fit-content; 289 | flex-grow: 1; 290 | } 291 | 292 | .mem-slot { 293 | cursor: pointer; 294 | } 295 | 296 | .mem-slot:hover { 297 | background-color: var(--mid-gray-50); 298 | } 299 | 300 | .register-panel { 301 | display: flex; 302 | flex-direction: column; 303 | gap: 10px; 304 | } 305 | 306 | .ignorable-line { 307 | color: var(--mid-gray); 308 | } 309 | 310 | .error-message { 311 | color: #cf2106; 312 | font-weight: bold; 313 | } 314 | 315 | .inline-c-source-line { 316 | color: var(--dark-blue); 317 | } 318 | 319 | .active_mem_slot .inline-c-source-line.selected-line, 320 | .active_mem_slot .inline-c-source-line.dependency-line { 321 | color: var(--primary-dark); 322 | } 323 | 324 | .selected-line, 325 | .ignorable-line.selected-line, 326 | .dependency-line.selected-line { 327 | background-color: var(--yellow-50); 328 | color: #000; 329 | } 330 | 331 | .active_mem_slot .normal-line, 332 | .active_mem_slot .inline-c-source-line { 333 | color: #888888; 334 | } 335 | 336 | .active_mem_slot .normal-line.selected-line, 337 | .active_mem_slot .normal-line.dependency-line { 338 | color: #000; 339 | } 340 | 341 | .active_mem_slot .selected-line .mem-slot, 342 | .dependency-line .mem-slot { 343 | font-weight: bold; 344 | } 345 | 346 | .dep-arrow, 347 | .log-line, 348 | .c-source-line { 349 | height: 20px; 350 | } 351 | 352 | .log-line, 353 | .c-line { 354 | display: flex; 355 | white-space: nowrap; 356 | } 357 | 358 | .line-indent { 359 | width: 20px; 360 | display: inline-block; 361 | border-left: 1px solid #dbdbdb; 362 | align-self: stretch; 363 | } 364 | 365 | .log-line:hover, 366 | .c-source-line:hover { 367 | background-color: var(--mid-gray-20); 368 | color: #000; 369 | } 370 | 371 | .log-line.selected-line:hover, 372 | .c-source-line.selected-line:hover { 373 | background-color: var(--yellow-70); 374 | } 375 | 376 | .dependency-line { 377 | background-color: var(--mid-gray-20); 378 | } 379 | 380 | .effect-write { 381 | background-color: var(--bright-red-20); 382 | } 383 | 384 | .effect-read { 385 | background-color: var(--light-blue-20); 386 | } 387 | 388 | .dependency-mem-slot { 389 | font-weight: bold; 390 | } 391 | 392 | #log-container .selected-mem-slot { 393 | border: 1px solid black; 394 | } 395 | 396 | #mem-slot-tooltip { 397 | position: fixed; 398 | display: none; 399 | background-color: white; 400 | padding: 5px 10px; 401 | font-family: monospace; 402 | font-size: 12px; 403 | z-index: 1000; 404 | pointer-events: none; 405 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 406 | border-style: solid; 407 | border-color: black; 408 | border-width: 1px; 409 | } 410 | 411 | #mem-slot-tooltip-arrow { 412 | content: ""; 413 | position: absolute; 414 | transform: translateX(-50%); 415 | border-width: 0 5px 5px 5px; 416 | border-style: solid; 417 | border-color: transparent transparent #333 transparent; 418 | } 419 | 420 | .scratched { 421 | color: gray; 422 | } 423 | 424 | /* Dependency Arrows */ 425 | 426 | #dependency-arrows { 427 | padding-top: 10px; 428 | padding-bottom: 10px; 429 | line-height: 1.4; 430 | text-align: right; 431 | user-select: none; 432 | min-width: 2em; 433 | white-space: pre; 434 | height: fit-content; 435 | } 436 | 437 | .dep-arrow { 438 | position: relative; 439 | line-height: 1.4; 440 | text-align: right; 441 | user-select: none; 442 | min-width: 2em; 443 | white-space: pre; 444 | height: fit-content; 445 | height: 20px; 446 | color: #000; 447 | } 448 | 449 | .dep-end::before { 450 | content: "\2514 \2500"; /*└─*/ 451 | padding-left: 10px; 452 | } 453 | 454 | .dep-start::before { 455 | content: "\250C \2500"; /*┌─*/ 456 | padding-left: 10px; 457 | } 458 | 459 | .dep-mid::before { 460 | content: "\251C \2500"; /*├─*/ 461 | padding-left: 10px; 462 | } 463 | 464 | .dep-track::before { 465 | content: "\2502 \00a0"; /*│ */ 466 | padding-left: 10px; 467 | } 468 | 469 | .dep-track.active-up:hover:before { 470 | content: "\25B2 \00a0"; /*▲ */ 471 | padding-left: 10px; 472 | } 473 | 474 | .dep-track.active-down:hover:before { 475 | content: "\25BC \00a0"; /*▼ */ 476 | padding-left: 10px; 477 | } 478 | 479 | /* State Panel */ 480 | 481 | .state-panel { 482 | background-color: var(--mid-gray-20); 483 | border-left: 2px solid var(--mid-gray); 484 | position: relative; 485 | font-family: "Roboto", sans-serif; 486 | } 487 | 488 | #state-panel { 489 | flex: 1; 490 | padding: 0px; 491 | position: relative; 492 | } 493 | 494 | #state-panel-content { 495 | overflow-y: scroll; 496 | height: 100%; 497 | } 498 | 499 | .panel-hidden { 500 | flex: none; 501 | width: 40px; 502 | text-align: center; 503 | } 504 | 505 | #state-panel-header { 506 | display: flex; 507 | flex-direction: row; 508 | margin-bottom: 10px; 509 | color: var(--primary-dark); 510 | height: 40px; 511 | padding: 11px; 512 | box-sizing: border-box; 513 | border-bottom: 2px solid var(--mid-gray); 514 | } 515 | 516 | #state-panel-header .bold { 517 | font-weight: bold; 518 | } 519 | 520 | #state-panel-header div { 521 | display: inline-block; 522 | margin-right: 32px; 523 | } 524 | 525 | .panel-header-active { 526 | cursor: pointer; 527 | } 528 | 529 | .panel-header-active:hover { 530 | text-decoration: underline; 531 | } 532 | 533 | #state-panel-table { 534 | padding: 0px 10px; 535 | } 536 | 537 | .state-panel table { 538 | width: 100%; 539 | border-collapse: collapse; 540 | font-family: monospace; 541 | font-size: 14px; 542 | } 543 | 544 | .state-panel td { 545 | padding: 4px 8px; 546 | border-bottom: 1px solid #ddd; 547 | } 548 | 549 | .state-panel tr { 550 | cursor: pointer; 551 | } 552 | 553 | .state-panel tr:hover { 554 | background-color: var(--mid-gray-20); 555 | } 556 | 557 | .state-panel tr.row-empty:hover { 558 | background-color: transparent; 559 | } 560 | 561 | .mem-slot-label { 562 | font-weight: bold; 563 | color: var(--primary-dark); 564 | width: 7ch; 565 | } 566 | 567 | .state-panel td:last-child { 568 | font-family: monospace; 569 | word-break: break-all; 570 | } 571 | 572 | #state-panel-content .selected-mem-slot, 573 | .state-panel .selected-mem-slot .mem-slot-label { 574 | font-weight: bold; 575 | color: #000; 576 | } 577 | 578 | /* Panel Hide/Show Button */ 579 | 580 | .hide-show-button.hidden.left .hide-show-tooltip { 581 | right: auto; 582 | left: 30px; 583 | } 584 | 585 | /* C Source Paste Popup */ 586 | .c-source-popup-overlay { 587 | position: fixed; 588 | top: 0; 589 | left: 0; 590 | width: 100%; 591 | height: 100%; 592 | background-color: rgba(0, 0, 0, 0.5); 593 | display: flex; 594 | justify-content: center; 595 | align-items: center; 596 | z-index: 1000; 597 | } 598 | 599 | .c-source-popup-content { 600 | background-color: white; 601 | padding: 20px; 602 | border-radius: 8px; 603 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 604 | max-width: 800px; 605 | width: 90%; 606 | } 607 | 608 | .c-source-textarea { 609 | width: 98%; 610 | height: 400px; 611 | margin-bottom: 10px; 612 | padding: 1%; 613 | border: 2px dashed var(--primary-dark); 614 | } 615 | 616 | .c-source-popup-error { 617 | margin-bottom: 10px; 618 | font-weight: bold; 619 | color: var(--bright-red); 620 | } 621 | 622 | .c-source-button { 623 | margin-right: 10px; 624 | } 625 | 626 | /* Footer */ 627 | 628 | #hint { 629 | padding: 10px 16px; 630 | color: var(--primary-dark); 631 | min-height: 20px; 632 | width: 100%; 633 | font-family: "Roboto", sans-serif; 634 | position: absolute; 635 | background-color: var(--mid-gray-20); 636 | border-top: 2px solid var(--mid-gray); 637 | bottom: 0px; 638 | } 639 | 640 | .hint-line { 641 | font-size: 14px; 642 | line-height: 20px; 643 | } 644 | 645 | .hint-line span { 646 | font-weight: bold; 647 | } 648 | 649 | /* Loading Spinner */ 650 | .loader-container { 651 | width: 100%; 652 | height: 100%; 653 | position: absolute; 654 | top: 0px; 655 | left: 0px; 656 | background-color: rgba(0, 0, 0, 0.2); 657 | } 658 | 659 | .loader-content { 660 | width: 80px; 661 | top: 50%; 662 | left: 50%; 663 | transform: translate(-50%, -50%); 664 | position: absolute; 665 | } 666 | 667 | .loader { 668 | aspect-ratio: 1; 669 | display: grid; 670 | -webkit-mask: conic-gradient(from 15deg, #0003, #000); 671 | mask: conic-gradient(from 15deg, #0003, #000); 672 | animation: load 1s steps(12) infinite; 673 | color: var(--dark-blue); 674 | width: 80px; 675 | } 676 | 677 | .loader, 678 | .loader:before, 679 | .loader:after { 680 | background: 681 | radial-gradient(closest-side at 50% 12.5%, currentColor 90%, #0000 98%) 50% 682 | 0/20% 80% repeat-y, 683 | radial-gradient(closest-side at 12.5% 50%, currentColor 90%, #0000 98%) 0 684 | 50%/80% 20% repeat-x; 685 | } 686 | 687 | .loader:before, 688 | .loader:after { 689 | content: ""; 690 | grid-area: 1/1; 691 | transform: rotate(30deg); 692 | } 693 | 694 | .loader:after { 695 | transform: rotate(60deg); 696 | } 697 | 698 | @keyframes load { 699 | from { 700 | transform: rotate(0turn); 701 | } 702 | to { 703 | transform: rotate(1turn); 704 | } 705 | } 706 | -------------------------------------------------------------------------------- /src/components.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import "@testing-library/jest-dom"; 5 | import { render, screen } from "@testing-library/react"; 6 | import { JmpInstruction, LogLineState, MemSlot } from "./components"; 7 | import { 8 | BpfJmpKind, 9 | BpfInstructionClass, 10 | BpfInstructionKind, 11 | BpfJmpCode, 12 | BpfOperand, 13 | OperandType, 14 | OpcodeSource, 15 | ParsedLine, 16 | ParsedLineType, 17 | BpfTargetJmpInstruction, 18 | BpfConditionalJmpInstruction, 19 | BpfJmpInstruction, 20 | BpfExitInstruction, 21 | BpfGotoJmpInstruction, 22 | parseLine, 23 | InstructionLine, 24 | BPF_SCRATCH_REGS, 25 | } from "./parser"; 26 | import { BpfState, makeValue } from "./analyzer"; 27 | import { Effect } from "./parser"; 28 | 29 | function createOp( 30 | type: OperandType, 31 | size: number, 32 | offset: number, 33 | id = "", 34 | ): BpfOperand { 35 | return { 36 | type, 37 | id, 38 | location: { 39 | offset, 40 | size, 41 | }, 42 | size: 0, // unused for MemSlot 43 | }; 44 | } 45 | 46 | function getEmptyLogLineState(): LogLineState { 47 | return { 48 | memSlotId: "", 49 | line: 0, 50 | cLine: "", 51 | }; 52 | } 53 | 54 | function dummyBpfState(frame: number = 0): BpfState { 55 | const state = new BpfState({}); 56 | state.frame = frame; 57 | for (const reg of BPF_SCRATCH_REGS) { 58 | state.values.set(reg, makeValue("", Effect.UPDATE)); 59 | } 60 | return state; 61 | } 62 | 63 | describe("MemSlot", () => { 64 | function createParsedLine(raw: string, idx: number): ParsedLine { 65 | return parseLine(raw, idx); 66 | } 67 | 68 | it("renders raw line when op is undefined", () => { 69 | const line = createParsedLine("test raw line", 0); 70 | render( 71 | , 78 | ); 79 | expect(screen.getByText("test raw line")).toBeInTheDocument(); 80 | }); 81 | 82 | it("renders the sliced memslot string when op is UNKNOWN", () => { 83 | const line = createParsedLine("test raw line", 0); 84 | render( 85 | , 92 | ); 93 | // screen.debug() 94 | expect(screen.getByText("line")).toBeInTheDocument(); 95 | }); 96 | 97 | it("renders the sliced memslot string when op is IMM", () => { 98 | const line = createParsedLine("test raw line", 0); 99 | render( 100 | , 107 | ); 108 | expect(screen.getByText("aw line")).toBeInTheDocument(); 109 | }); 110 | 111 | it("renders a RegSpan when op is REG", () => { 112 | const line = createParsedLine( 113 | "5: (b7) r7 = 1 ; R7_w=1 refs=2", 114 | 0, 115 | ); 116 | render( 117 | , 124 | ); 125 | const divs = document.getElementsByTagName("div"); 126 | expect(divs.length).toBe(1); 127 | expect(divs[0].innerHTML).toBe( 128 | 'r7', 129 | ); 130 | }); 131 | 132 | it("renders a RegSpan when op is FP", () => { 133 | const line = createParsedLine( 134 | "1768: (79) r1 = *(u64 *)(r10 -8) ; frame2: R1_w=scalar(umax=511,var_off=(0x0; 0x1ff)) R10=fp0", 135 | 0, 136 | ); 137 | render( 138 | , 145 | ); 146 | const divs = document.getElementsByTagName("div"); 147 | expect(divs.length).toBe(1); 148 | expect(divs[0].innerHTML).toBe( 149 | '*(u64 *)(r10 -8)', 150 | ); 151 | }); 152 | 153 | it("renders a RegSpan and mem slot strings when op is MEM", () => { 154 | const line = createParsedLine( 155 | "1609: (61) r2 = *(u32 *)(r2 +0) ; frame1: R2_w=0", 156 | 0, 157 | ); 158 | const op = createOp(OperandType.MEM, 15, -38, "MEM"); 159 | op.memref = { 160 | reg: "r2", 161 | offset: 0, 162 | }; 163 | render( 164 | , 171 | ); 172 | const divs = document.getElementsByTagName("div"); 173 | expect(divs.length).toBe(1); 174 | expect(divs[0].innerHTML).toBe( 175 | '*(u32 *)(r2 +0)', 176 | ); 177 | }); 178 | 179 | it("renders a nested RegSpan when op is MEM the references an FP", () => { 180 | const line = createParsedLine("530: (79) r6 = *(u64 *)(r1 -8)", 0); 181 | const op = createOp(OperandType.MEM, 15, -15, "MEM"); 182 | op.memref = { 183 | reg: "r1", 184 | offset: -8, 185 | }; 186 | const bpfState = dummyBpfState(); 187 | bpfState.values.set("r1", { value: "fp-16", effect: Effect.WRITE }); 188 | render( 189 | , 196 | ); 197 | const divs = document.getElementsByTagName("div"); 198 | expect(divs.length).toBe(1); 199 | expect(divs[0].innerHTML).toBe( 200 | '*(u64 *)(r1 -8)', 201 | ); 202 | }); 203 | }); 204 | 205 | describe("JmpInstruction", () => { 206 | function createTargetJmpIns( 207 | jmpCode: BpfJmpCode, 208 | jmpKind: BpfJmpKind.HELPER_CALL | BpfJmpKind.SUBPROGRAM_CALL, 209 | target: string = "pc+3", 210 | ): BpfTargetJmpInstruction { 211 | return { 212 | kind: BpfInstructionKind.JMP, 213 | jmpKind, 214 | opcode: { 215 | iclass: BpfInstructionClass.JMP, 216 | code: jmpCode, 217 | source: OpcodeSource.K, 218 | }, 219 | target, 220 | reads: [], 221 | writes: [], 222 | // corresponds to "pc+3" in ParsedLine.raw 223 | location: { 224 | offset: -4, 225 | size: 4, 226 | }, 227 | }; 228 | } 229 | 230 | function createGotoJmpIns( 231 | goto: string, 232 | jmpCode: BpfJmpCode, 233 | jmpKind: 234 | | BpfJmpKind.MAY_GOTO 235 | | BpfJmpKind.GOTO_OR_NOP 236 | | BpfJmpKind.UNCONDITIONAL_GOTO, 237 | ): BpfGotoJmpInstruction { 238 | return { 239 | kind: BpfInstructionKind.JMP, 240 | goto, 241 | jmpKind, 242 | opcode: { 243 | iclass: BpfInstructionClass.JMP, 244 | code: jmpCode, 245 | source: OpcodeSource.K, 246 | }, 247 | target: "pc+3", 248 | reads: [], 249 | writes: [], 250 | }; 251 | } 252 | 253 | function createLine(bpfIns: BpfJmpInstruction): InstructionLine { 254 | return { 255 | raw: "call pc+3", 256 | idx: 0, 257 | bpfIns, 258 | bpfStateExprs: [], 259 | type: ParsedLineType.INSTRUCTION, 260 | }; 261 | } 262 | 263 | it("renders an exit", () => { 264 | const ins: BpfExitInstruction = { 265 | kind: BpfInstructionKind.JMP, 266 | jmpKind: BpfJmpKind.EXIT, 267 | opcode: { 268 | iclass: BpfInstructionClass.JMP, 269 | code: BpfJmpCode.JEQ, 270 | source: OpcodeSource.K, 271 | }, 272 | reads: [], 273 | writes: [], 274 | }; 275 | const line = createLine(ins); 276 | 277 | render( 278 | , 285 | ); 286 | const divs = document.getElementsByTagName("div"); 287 | expect(divs.length).toBe(1); 288 | expect(divs[0].innerHTML).toBe("} exit ; return to stack frame 1"); 289 | }); 290 | 291 | it("renders a subprogram and target", () => { 292 | const ins = createTargetJmpIns(BpfJmpCode.JA, BpfJmpKind.SUBPROGRAM_CALL); 293 | const line = createLine(ins); 294 | 295 | render( 296 | , 303 | ); 304 | const divs = document.getElementsByTagName("div"); 305 | expect(divs.length).toBe(1); 306 | expect(divs[0].innerHTML).toBe("pc+3() { ; enter new stack frame 1"); 307 | }); 308 | 309 | it("renders a helper call and target", () => { 310 | const ins = createTargetJmpIns(BpfJmpCode.JA, BpfJmpKind.HELPER_CALL); 311 | const line = createLine(ins); 312 | 313 | render( 314 | , 321 | ); 322 | const divs = document.getElementsByTagName("div"); 323 | expect(divs.length).toBe(1); 324 | expect(divs[0].innerHTML).toBe( 325 | 'r0 = pc+3()', 326 | ); 327 | }); 328 | 329 | it("renders an unconditional goto and target", () => { 330 | const ins = createGotoJmpIns( 331 | "goto", 332 | BpfJmpCode.JA, 333 | BpfJmpKind.UNCONDITIONAL_GOTO, 334 | ); 335 | const line = createLine(ins); 336 | 337 | render( 338 | , 345 | ); 346 | const divs = document.getElementsByTagName("div"); 347 | expect(divs.length).toBe(1); 348 | expect(divs[0].innerHTML).toBe("goto pc+3"); 349 | }); 350 | 351 | it("renders a may_goto and target", () => { 352 | const ins = createGotoJmpIns( 353 | "may_goto", 354 | BpfJmpCode.JCOND, 355 | BpfJmpKind.MAY_GOTO, 356 | ); 357 | const line = createLine(ins); 358 | 359 | render( 360 | , 367 | ); 368 | const divs = document.getElementsByTagName("div"); 369 | expect(divs.length).toBe(1); 370 | expect(divs[0].innerHTML).toBe("may_goto pc+3"); 371 | }); 372 | 373 | it("renders a goto_or_nop and target", () => { 374 | const ins = createGotoJmpIns( 375 | "goto_or_nop", 376 | BpfJmpCode.JCOND, 377 | BpfJmpKind.GOTO_OR_NOP, 378 | ); 379 | const line = createLine(ins); 380 | 381 | render( 382 | , 389 | ); 390 | const divs = document.getElementsByTagName("div"); 391 | expect(divs.length).toBe(1); 392 | expect(divs[0].innerHTML).toBe("goto_or_nop pc+3"); 393 | }); 394 | 395 | it("renders a mem slot wrapped goto", () => { 396 | const ins: BpfConditionalJmpInstruction = { 397 | kind: BpfInstructionKind.JMP, 398 | jmpKind: BpfJmpKind.CONDITIONAL_GOTO, 399 | opcode: { 400 | iclass: BpfInstructionClass.JMP, 401 | code: BpfJmpCode.JEQ, 402 | source: OpcodeSource.K, 403 | }, 404 | target: "pc+3", 405 | cond: { 406 | left: createOp(OperandType.REG, 2, -45, "r7"), 407 | op: "==", 408 | right: createOp(OperandType.REG, 2, -45, "r8"), 409 | }, 410 | reads: [], 411 | writes: [], 412 | }; 413 | const line = createLine(ins); 414 | 415 | render( 416 | , 423 | ); 424 | const divs = document.getElementsByTagName("div"); 425 | expect(divs.length).toBe(1); 426 | expect(divs[0].innerHTML).toBe( 427 | 'if (r7 == r8) goto pc+3', 428 | ); 429 | }); 430 | 431 | function setCallArgValue(state: BpfState, arg: string, preCallValue: string) { 432 | state.values.set(arg, makeValue("", Effect.UPDATE, preCallValue)); 433 | } 434 | 435 | describe("CallHtml argument counting", () => { 436 | it("shows 3 arguments when r4 and r5 are scratched", () => { 437 | const ins = createTargetJmpIns( 438 | BpfJmpCode.JA, 439 | BpfJmpKind.HELPER_CALL, 440 | "bpf_helper#123", 441 | ); 442 | const line = createLine(ins); 443 | const state = new BpfState({}); 444 | 445 | setCallArgValue(state, "r1", "ctx()"); 446 | setCallArgValue(state, "r2", "16"); 447 | setCallArgValue(state, "r3", "fp-8"); 448 | setCallArgValue(state, "r4", ""); 449 | setCallArgValue(state, "r5", ""); 450 | 451 | render( 452 | , 459 | ); 460 | const divs = document.getElementsByTagName("div"); 461 | expect(divs.length).toBe(1); 462 | 463 | // Should show r0 = call bpf_helper#123(r1, r2, r3) 464 | const innerHTML = divs[0].innerHTML; 465 | expect(innerHTML).toContain("r1"); 466 | expect(innerHTML).toContain("r2"); 467 | expect(innerHTML).toContain("r3"); 468 | expect(innerHTML).not.toMatch(/r4.*,/); 469 | expect(innerHTML).not.toMatch(/r5.*,/); 470 | }); 471 | 472 | it("shows 4 arguments when only r4 is not scratched", () => { 473 | const ins = createTargetJmpIns( 474 | BpfJmpCode.JA, 475 | BpfJmpKind.HELPER_CALL, 476 | "bpf_helper#123", 477 | ); 478 | const line = createLine(ins); 479 | const state = new BpfState({}); 480 | 481 | setCallArgValue(state, "r1", ""); 482 | setCallArgValue(state, "r2", ""); 483 | setCallArgValue(state, "r3", ""); 484 | setCallArgValue(state, "r4", "scalar()"); 485 | setCallArgValue(state, "r5", ""); 486 | 487 | render( 488 | , 495 | ); 496 | const divs = document.getElementsByTagName("div"); 497 | expect(divs.length).toBe(1); 498 | 499 | // Should show r0 = call bpf_helper#123(r1, r2, r3) 500 | const innerHTML = divs[0].innerHTML; 501 | expect(innerHTML).toContain("r1"); 502 | expect(innerHTML).toContain("r2"); 503 | expect(innerHTML).toContain("r3"); 504 | expect(innerHTML).toContain("r4"); 505 | expect(innerHTML).not.toMatch(/r5.*,/); 506 | }); 507 | }); 508 | 509 | describe("global function call rendering", () => { 510 | it("renders global function name", () => { 511 | // After analyzer transformation, a global function call becomes a HELPER_CALL 512 | // with the function name as the target (e.g., "my_global_func" instead of "pc+10") 513 | const ins = createTargetJmpIns( 514 | BpfJmpCode.CALL, 515 | BpfJmpKind.HELPER_CALL, 516 | "my_global_func", 517 | ); 518 | const line = createLine(ins); 519 | const state = new BpfState({}); 520 | setCallArgValue(state, "r1", "ctx()"); 521 | setCallArgValue(state, "r2", "buffer_ptr"); 522 | setCallArgValue(state, "r3", ""); 523 | 524 | render( 525 | , 532 | ); 533 | const divs = document.getElementsByTagName("div"); 534 | expect(divs.length).toBe(1); 535 | 536 | const innerHTML = divs[0].innerHTML; 537 | expect(innerHTML).toContain("my_global_func"); 538 | expect(innerHTML).toContain("r1"); 539 | expect(innerHTML).toContain("r2"); 540 | }); 541 | }); 542 | }); 543 | -------------------------------------------------------------------------------- /src/__snapshots__/App.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`App renders the correct starting elements 1`] = ` 4 |
5 |
8 |
11 | 52 |