commit 6d48600ea099a0ac3e99a3970dd79f989f3f9b84 (tree)
parent 75c9936737a6ba991d4ef187ddc9d51bc0ad0998
Author: Veikka Tuominen <git@vexu.eu>
Date: Thu, 28 Apr 2022 18:54:09 +0300
Merge pull request #10717 from gh-fork-dump/seccomp-bits
Add Seccomp bits for linux
Diffstat:
6 files changed, 1285 insertions(+), 1 deletion(-)
diff --git a/lib/std/elf.zig b/lib/std/elf.zig
@@ -1482,6 +1482,12 @@ pub const EM = enum(u16) {
/// Linux kernel bpf virtual machine
_BPF = 247,
+ /// C-SKY
+ _CSKY = 252,
+
+ /// Fujitsu FR-V
+ _FRV = 0x5441,
+
_,
pub fn toTargetCpuArch(em: EM) ?std.Target.Cpu.Arch {
diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig
@@ -91,6 +91,7 @@ pub const tls = @import("linux/tls.zig");
pub const pie = @import("linux/start_pie.zig");
pub const BPF = @import("linux/bpf.zig");
pub const IOCTL = @import("linux/ioctl.zig");
+pub const SECCOMP = @import("linux/seccomp.zig");
pub const MAP = struct {
pub usingnamespace arch_bits.MAP;
@@ -1691,6 +1692,10 @@ pub fn perf_event_open(
);
}
+pub fn seccomp(operation: u32, flags: u32, args: ?*const anyopaque) usize {
+ return syscall3(.seccomp, operation, flags, @ptrToInt(args));
+}
+
pub const E = switch (native_arch) {
.mips, .mipsel => @import("linux/errno/mips.zig").E,
.sparc, .sparcel, .sparcv9 => @import("linux/errno/sparc.zig").E,
@@ -5409,3 +5414,55 @@ pub const PERF = struct {
pub const IOC_FLAG_GROUP = 1;
};
+
+// TODO: Add the rest of the AUDIT defines?
+pub const AUDIT = struct {
+ pub const ARCH = enum(u32) {
+ const _64BIT = 0x80000000;
+ const _LE = 0x40000000;
+
+ pub const current = switch (native_arch) {
+ .i386 => .I386,
+ .x86_64 => .X86_64,
+ .aarch64 => .AARCH64,
+ .arm, .thumb => .ARM,
+ .riscv64 => .RISCV64,
+ .sparcv9 => .SPARC64,
+ .mips => .MIPS,
+ .mipsel => .MIPSEL,
+ .powerpc => .PPC,
+ .powerpc64 => .PPC64,
+ .powerpc64le => .PPC64LE,
+ else => undefined,
+ };
+
+ AARCH64 = toAudit(.aarch64),
+ ARM = toAudit(.arm),
+ ARMEB = toAudit(.armeb),
+ CSKY = toAudit(.csky),
+ HEXAGON = @enumToInt(std.elf.EM._HEXAGON),
+ I386 = toAudit(.i386),
+ M68K = toAudit(.m68k),
+ MIPS = toAudit(.mips),
+ MIPSEL = toAudit(.mips) | _LE,
+ MIPS64 = toAudit(.mips64),
+ MIPSEL64 = toAudit(.mips64) | _LE,
+ PPC = toAudit(.powerpc),
+ PPC64 = toAudit(.powerpc64),
+ PPC64LE = toAudit(.powerpc64le),
+ RISCV32 = toAudit(.riscv32),
+ RISCV64 = toAudit(.riscv64),
+ S390X = toAudit(.s390x),
+ SPARC = toAudit(.sparc),
+ SPARC64 = toAudit(.sparcv9),
+ X86_64 = toAudit(.x86_64),
+
+ fn toAudit(arch: std.Target.Cpu.Arch) u32 {
+ var res: u32 = @enumToInt(arch.toElfMachine());
+ if (arch.endian() == .Little) res |= _LE;
+ if (arch.ptrBitWidth() == 64) res |= _64BIT;
+
+ return res;
+ }
+ };
+};
diff --git a/lib/std/os/linux/seccomp.zig b/lib/std/os/linux/seccomp.zig
@@ -0,0 +1,212 @@
+//! API bits for the Secure Computing facility in the Linux kernel, which allows
+//! processes to restrict access to the system call API.
+//!
+//! Seccomp started life with a single "strict" mode, which only allowed calls
+//! to read(2), write(2), _exit(2) and sigreturn(2). It turns out that this
+//! isn't that useful for general-purpose applications, and so a mode that
+//! utilizes user-supplied filters mode was added.
+//!
+//! Seccomp filters are classic BPF programs, which means that all the
+//! information under `std.x.net.bpf` applies here as well. Conceptually, a
+//! seccomp program is attached to the kernel and is executed on each syscall.
+//! The "packet" being validated is the `data` structure, and the verdict is an
+//! action that the kernel performs on the calling process. The actions are
+//! variations on a "pass" or "fail" result, where a pass allows the syscall to
+//! continue and a fail blocks the syscall and returns some sort of error value.
+//! See the full list of actions under ::RET for more information. Finally, only
+//! word-sized, absolute loads (`ld [k]`) are supported to read from the `data`
+//! structure.
+//!
+//! There are some issues with the filter API that have traditionally made
+//! writing them a pain:
+//!
+//! 1. Each CPU architecture supported by Linux has its own unique ABI and
+//! syscall API. It is not guaranteed that the syscall numbers and arguments
+//! are the same across architectures, or that they're even implemted. Thus,
+//! filters cannot be assumed to be portable without consulting documentation
+//! like syscalls(2) and testing on target hardware. This also requires
+//! checking the value of `data.arch` to make sure that a filter was compiled
+//! for the correct architecture.
+//! 2. Many syscalls take an `unsigned long` or `size_t` argument, the size of
+//! which is dependant on the ABI. Since BPF programs execute in a 32-bit
+//! machine, validation of 64-bit arguments necessitates two load-and-compare
+//! instructions for the upper and lower words.
+//! 3. A further wrinkle to the above is endianess. Unlike network packets,
+//! syscall data shares the endianess of the target machine. A filter
+//! compiled on a little-endian machine will not work on a big-endian one,
+//! and vice-versa. For example: Checking the upper 32-bits of `data.arg1`
+//! requires a load at `@offsetOf(data, "arg1") + 4` on big-endian systems
+//! and `@offsetOf(data, "arg1")` on little-endian systems. Endian-portable
+//! filters require adjusting these offsets at compile time, similar to how
+//! e.g. OpenSSH does[1].
+//! 4. Syscalls with userspace implementations via the vDSO cannot be traced or
+//! filtered. The vDSO can be disabled or just ignored, which must be taken
+//! into account when writing filters.
+//! 5. Software libraries - especially dynamically loaded ones - tend to use
+//! more of the syscall API over time, thus filters must evolve with them.
+//! Static filters can result in reduced or even broken functionality when
+//! calling newer code from these libraries. This is known to happen with
+//! critical libraries like glibc[2].
+//!
+//! Some of these issues can be mitigated with help from Zig and the standard
+//! library. Since the target CPU is known at compile time, the proper syscall
+//! numbers are mixed into the `os` namespace under `std.os.SYS (see the code
+//! for `arch_bits` in `os/linux.zig`). Referencing an unimplemented syscall
+//! would be a compile error. Endian offsets can also be defined in a similar
+//! manner to the OpenSSH example:
+//!
+//! ```zig
+//! const offset = if (native_endian == .Little) struct {
+//! pub const low = 0;
+//! pub const high = @sizeOf(u32);
+//! } else struct {
+//! pub const low = @sizeOf(u32);
+//! pub const high = 0;
+//! };
+//! ```
+//!
+//! Unfortunately, there is no easy solution for issue 5. The most reliable
+//! strategy is to keep testing; test newer Zig versions, different libcs,
+//! different distros, and design your filter to accomidate all of them.
+//! Alternatively, you could inject a filter at runtime. Since filters are
+//! preserved across execve(2), a filter could be setup before executing your
+//! program, without your program having any knowledge of this happening. This
+//! is the method used by systemd[3] and Cloudflare's sandbox library[4].
+//!
+//! [1]: https://github.com/openssh/openssh-portable/blob/master/sandbox-seccomp-filter.c#L81
+//! [2]: https://sourceware.org/legacy-ml/libc-alpha/2017-11/msg00246.html
+//! [3]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=
+//! [4]: https://github.com/cloudflare/sandbox
+//!
+//! See Also
+//! - seccomp(2), seccomp_unotify(2)
+//! - https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html
+const IOCTL = @import("ioctl.zig");
+
+// Modes for the prctl(2) form `prctl(PR_SET_SECCOMP, mode)`
+pub const MODE = struct {
+ /// Seccomp not in use.
+ pub const DISABLED = 0;
+ /// Uses a hard-coded filter.
+ pub const STRICT = 1;
+ /// Uses a user-supplied filter.
+ pub const FILTER = 2;
+};
+
+// Operations for the seccomp(2) form `seccomp(operation, flags, args)`
+pub const SET_MODE_STRICT = 0;
+pub const SET_MODE_FILTER = 1;
+pub const GET_ACTION_AVAIL = 2;
+pub const GET_NOTIF_SIZES = 3;
+
+/// Bitflags for the SET_MODE_FILTER operation.
+pub const FILTER_FLAG = struct {
+ pub const TSYNC = 1 << 0;
+ pub const LOG = 1 << 1;
+ pub const SPEC_ALLOW = 1 << 2;
+ pub const NEW_LISTENER = 1 << 3;
+ pub const TSYNC_ESRCH = 1 << 4;
+};
+
+/// Action values for seccomp BPF programs.
+/// The lower 16-bits are for optional return data.
+/// The upper 16-bits are ordered from least permissive values to most.
+pub const RET = struct {
+ /// Kill the process.
+ pub const KILL_PROCESS = 0x80000000;
+ /// Kill the thread.
+ pub const KILL_THREAD = 0x00000000;
+ pub const KILL = KILL_THREAD;
+ /// Disallow and force a SIGSYS.
+ pub const TRAP = 0x00030000;
+ /// Return an errno.
+ pub const ERRNO = 0x00050000;
+ /// Forward the syscall to a userspace supervisor to make a decision.
+ pub const USER_NOTIF = 0x7fc00000;
+ /// Pass to a tracer or disallow.
+ pub const TRACE = 0x7ff00000;
+ /// Allow after logging.
+ pub const LOG = 0x7ffc0000;
+ /// Allow.
+ pub const ALLOW = 0x7fff0000;
+
+ // Masks for the return value sections.
+ pub const ACTION_FULL = 0xffff0000;
+ pub const ACTION = 0x7fff0000;
+ pub const DATA = 0x0000ffff;
+};
+
+pub const IOCTL_NOTIF = struct {
+ pub const RECV = IOCTL.IOWR('!', 0, notif);
+ pub const SEND = IOCTL.IOWR('!', 1, notif_resp);
+ pub const ID_VALID = IOCTL.IOW('!', 2, u64);
+ pub const ADDFD = IOCTL.IOW('!', 3, notif_addfd);
+};
+
+/// Tells the kernel that the supervisor allows the syscall to continue.
+pub const USER_NOTIF_FLAG_CONTINUE = 1 << 0;
+
+/// See seccomp_unotify(2).
+pub const ADDFD_FLAG = struct {
+ pub const SETFD = 1 << 0;
+ pub const SEND = 1 << 1;
+};
+
+pub const data = extern struct {
+ /// The system call number.
+ nr: c_int,
+ /// The CPU architecture/system call convention.
+ /// One of the values defined in `std.os.linux.AUDIT`.
+ arch: u32,
+ instruction_pointer: u64,
+ arg0: u64,
+ arg1: u64,
+ arg2: u64,
+ arg3: u64,
+ arg4: u64,
+ arg5: u64,
+};
+
+/// Used with the ::GET_NOTIF_SIZES command to check if the kernel structures
+/// have changed.
+pub const notif_sizes = extern struct {
+ /// Size of ::notif.
+ notif: u16,
+ /// Size of ::resp.
+ notif_resp: u16,
+ /// Size of ::data.
+ data: u16,
+};
+
+pub const notif = extern struct {
+ /// Unique notification cookie for each filter.
+ id: u64,
+ /// ID of the thread that triggered the notification.
+ pid: u32,
+ /// Bitmask for event information. Currently set to zero.
+ flags: u32,
+ /// The current system call data.
+ data: data,
+};
+
+/// The decision payload the supervisor process sends to the kernel.
+pub const notif_resp = extern struct {
+ /// The filter cookie.
+ id: u64,
+ /// The return value for a spoofed syscall.
+ val: i64,
+ /// Set to zero for a spoofed success or a negative error number for a
+ /// failure.
+ @"error": i32,
+ /// Bitmask containing the decision. Either USER_NOTIF_FLAG_CONTINUE to
+ /// allow the syscall or zero to spoof the return values.
+ flags: u32,
+};
+
+pub const notif_addfd = extern struct {
+ id: u64,
+ flags: u32,
+ srcfd: u32,
+ newfd: u32,
+ newfd_flags: u32,
+};
diff --git a/lib/std/target.zig b/lib/std/target.zig
@@ -963,7 +963,7 @@ pub const Target = struct {
.amdgcn => ._NONE,
.bpfel => ._BPF,
.bpfeb => ._BPF,
- .csky => ._NONE,
+ .csky => ._CSKY,
.sparcv9 => ._SPARCV9,
.s390x => ._S390,
.ve => ._NONE,
diff --git a/lib/std/x.zig b/lib/std/x.zig
@@ -9,6 +9,7 @@ pub const os = struct {
pub const net = struct {
pub const ip = @import("x/net/ip.zig");
pub const tcp = @import("x/net/tcp.zig");
+ pub const bpf = @import("x/net/bpf.zig");
};
test {
diff --git a/lib/std/x/net/bpf.zig b/lib/std/x/net/bpf.zig
@@ -0,0 +1,1008 @@
+//! This package provides instrumentation for creating Berkeley Packet Filter[1]
+//! (BPF) programs, along with a simulator for running them.
+//!
+//! BPF is a mechanism for cheap, in-kernel packet filtering. Programs are
+//! attached to a network device and executed for every packet that flows
+//! through it. The program must then return a verdict: the amount of packet
+//! bytes that the kernel should copy into userspace. Execution speed is
+//! achieved by having programs run in a limited virtual machine, which has the
+//! added benefit of graceful failure in the face of buggy programs.
+//!
+//! The BPF virtual machine has a 32-bit word length and a small number of
+//! word-sized registers:
+//!
+//! - The accumulator, `a`: The source/destination of arithmetic and logic
+//! operations.
+//! - The index register, `x`: Used as an offset for indirect memory access and
+//! as a comparison value for conditional jumps.
+//! - The scratch memory store, `M[0]..M[15]`: Used for saving the value of a/x
+//! for later use.
+//!
+//! The packet being examined is an array of bytes, and is addressed using plain
+//! array subscript notation, e.g. [10] for the byte at offset 10. An implicit
+//! program counter, `pc`, is intialized to zero and incremented for each instruction.
+//!
+//! The machine has a fixed instruction set with the following form, where the
+//! numbers represent bit length:
+//!
+//! ```
+//! ┌───────────┬──────┬──────┐
+//! │ opcode:16 │ jt:8 │ jt:8 │
+//! ├───────────┴──────┴──────┤
+//! │ k:32 │
+//! └─────────────────────────┘
+//! ```
+//!
+//! The `opcode` indicates the instruction class and its addressing mode.
+//! Opcodes are generated by performing binary addition on the 8-bit class and
+//! mode constants. For example, the opcode for loading a byte from the packet
+//! at X + 2, (`ldb [x + 2]`), is:
+//!
+//! ```
+//! LD | IND | B = 0x00 | 0x40 | 0x20
+//! = 0x60
+//! ```
+//!
+//! `jt` is an offset used for conditional jumps, and increments the program
+//! counter by its amount if the comparison was true. Conversely, `jf`
+//! increments the counter if it was false. These fields are ignored in all
+//! other cases. `k` is a generic variable used for various purposes, most
+//! commonly as some sort of constant.
+//!
+//! This package contains opcode extensions used by different implementations,
+//! where "extension" is anything outside of the original that was imported into
+//! 4.4BSD[2]. These are marked with "EXTENSION", along with a list of
+//! implementations that use them.
+//!
+//! Most of the doc-comments use the BPF assembly syntax as described in the
+//! original paper[1]. For the sake of completeness, here is the complete
+//! instruction set, along with the extensions:
+//!
+//!```
+//! opcode addressing modes
+//! ld #k #len M[k] [k] [x + k]
+//! ldh [k] [x + k]
+//! ldb [k] [x + k]
+//! ldx #k #len M[k] 4 * ([k] & 0xf) arc4random()
+//! st M[k]
+//! stx M[k]
+//! jmp L
+//! jeq #k, Lt, Lf
+//! jgt #k, Lt, Lf
+//! jge #k, Lt, Lf
+//! jset #k, Lt, Lf
+//! add #k x
+//! sub #k x
+//! mul #k x
+//! div #k x
+//! or #k x
+//! and #k x
+//! lsh #k x
+//! rsh #k x
+//! neg #k x
+//! mod #k x
+//! xor #k x
+//! ret #k a
+//! tax
+//! txa
+//! ```
+//!
+//! Finally, a note on program design. The lack of backwards jumps leads to a
+//! "return early, return often" control flow. Take for example the program
+//! generated from the tcpdump filter `ip`:
+//!
+//! ```
+//! (000) ldh [12] ; Ethernet Packet Type
+//! (001) jeq #0x86dd, 2, 7 ; ETHERTYPE_IPV6
+//! (002) ldb [20] ; IPv6 Next Header
+//! (003) jeq #0x6, 10, 4 ; TCP
+//! (004) jeq #0x2c, 5, 11 ; IPv6 Fragment Header
+//! (005) ldb [54] ; TCP Source Port
+//! (006) jeq #0x6, 10, 11 ; IPPROTO_TCP
+//! (007) jeq #0x800, 8, 11 ; ETHERTYPE_IP
+//! (008) ldb [23] ; IPv4 Protocol
+//! (009) jeq #0x6, 10, 11 ; IPPROTO_TCP
+//! (010) ret #262144 ; copy 0x40000
+//! (011) ret #0 ; skip packet
+//! ```
+//!
+//! Here we can make a few observations:
+//!
+//! - The problem "filter only tcp packets" has essentially been transformed
+//! into a series of layer checks.
+//! - There are two distinct branches in the code, one for validating IPv4
+//! headers and one for IPv6 headers.
+//! - Most conditional jumps in these branches lead directly to the last two
+//! instructions, a pass or fail. Thus the goal of a program is to find the
+//! fastest route to a pass/fail comparison.
+//!
+//! [1]: S. McCanne and V. Jacobson, "The BSD Packet Filter: A New Architecture
+//! for User-level Packet Capture", Proceedings of the 1993 Winter USENIX.
+//! [2]: https://minnie.tuhs.org/cgi-bin/utree.pl?file=4.4BSD/usr/src/sys/net/bpf.h
+const std = @import("std");
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+const mem = std.mem;
+const math = std.math;
+const random = std.crypto.random;
+const assert = std.debug.assert;
+const expectEqual = std.testing.expectEqual;
+const expectError = std.testing.expectError;
+const expect = std.testing.expect;
+
+// instruction classes
+/// ld, ldh, ldb: Load data into a.
+pub const LD = 0x00;
+/// ldx: Load data into x.
+pub const LDX = 0x01;
+/// st: Store into scratch memory the value of a.
+pub const ST = 0x02;
+/// st: Store into scratch memory the value of x.
+pub const STX = 0x03;
+/// alu: Wrapping arithmetic/bitwise operations on a using the value of k/x.
+pub const ALU = 0x04;
+/// jmp, jeq, jgt, je, jset: Increment the program counter based on a comparison
+/// between k/x and the accumulator.
+pub const JMP = 0x05;
+/// ret: Return a verdict using the value of k/the accumulator.
+pub const RET = 0x06;
+/// tax, txa: Register value copying between X and a.
+pub const MISC = 0x07;
+
+// Size of data to be loaded from the packet.
+/// ld: 32-bit full word.
+pub const W = 0x00;
+/// ldh: 16-bit half word.
+pub const H = 0x08;
+/// ldb: Single byte.
+pub const B = 0x10;
+
+// Addressing modes used for loads to a/x.
+/// #k: The immediate value stored in k.
+pub const IMM = 0x00;
+/// [k]: The value at offset k in the packet.
+pub const ABS = 0x20;
+/// [x + k]: The value at offset x + k in the packet.
+pub const IND = 0x40;
+/// M[k]: The value of the k'th scratch memory register.
+pub const MEM = 0x60;
+/// #len: The size of the packet.
+pub const LEN = 0x80;
+/// 4 * ([k] & 0xf): Four times the low four bits of the byte at offset k in the
+/// packet. This is used for efficiently loading the header length of an IP
+/// packet.
+pub const MSH = 0xa0;
+/// arc4random: 32-bit integer generated from a CPRNG (see arc4random(3)) loaded into a.
+/// EXTENSION. Defined for:
+/// - OpenBSD.
+pub const RND = 0xc0;
+
+// Modifiers for different instruction classes.
+/// Use the value of k for alu operations (add #k).
+/// Compare against the value of k for jumps (jeq #k, Lt, Lf).
+/// Return the value of k for returns (ret #k).
+pub const K = 0x00;
+/// Use the value of x for alu operations (add x).
+/// Compare against the value of X for jumps (jeq x, Lt, Lf).
+pub const X = 0x08;
+/// Return the value of a for returns (ret a).
+pub const A = 0x10;
+
+// ALU Operations on a using the value of k/x.
+// All arithmetic operations are defined to overflow the value of a.
+/// add: a = a + k
+/// a = a + x.
+pub const ADD = 0x00;
+/// sub: a = a - k
+/// a = a - x.
+pub const SUB = 0x10;
+/// mul: a = a * k
+/// a = a * x.
+pub const MUL = 0x20;
+/// div: a = a / k
+/// a = a / x.
+/// Truncated division.
+pub const DIV = 0x30;
+/// or: a = a | k
+/// a = a | x.
+pub const OR = 0x40;
+/// and: a = a & k
+/// a = a & x.
+pub const AND = 0x50;
+/// lsh: a = a << k
+/// a = a << x.
+/// a = a << k, a = a << x.
+pub const LSH = 0x60;
+/// rsh: a = a >> k
+/// a = a >> x.
+pub const RSH = 0x70;
+/// neg: a = -a.
+/// Note that this isn't a binary negation, rather the value of `~a + 1`.
+pub const NEG = 0x80;
+/// mod: a = a % k
+/// a = a % x.
+/// EXTENSION. Defined for:
+/// - Linux.
+/// - NetBSD + Minix 3.
+/// - FreeBSD and derivitives.
+pub const MOD = 0x90;
+/// xor: a = a ^ k
+/// a = a ^ x.
+/// EXTENSION. Defined for:
+/// - Linux.
+/// - NetBSD + Minix 3.
+/// - FreeBSD and derivitives.
+pub const XOR = 0xa0;
+
+// Jump operations using a comparison between a and x/k.
+/// jmp L: pc += k.
+/// No comparison done here.
+pub const JA = 0x00;
+/// jeq #k, Lt, Lf: pc += (a == k) ? jt : jf.
+/// jeq x, Lt, Lf: pc += (a == x) ? jt : jf.
+pub const JEQ = 0x10;
+/// jgt #k, Lt, Lf: pc += (a > k) ? jt : jf.
+/// jgt x, Lt, Lf: pc += (a > x) ? jt : jf.
+pub const JGT = 0x20;
+/// jge #k, Lt, Lf: pc += (a >= k) ? jt : jf.
+/// jge x, Lt, Lf: pc += (a >= x) ? jt : jf.
+pub const JGE = 0x30;
+/// jset #k, Lt, Lf: pc += (a & k > 0) ? jt : jf.
+/// jset x, Lt, Lf: pc += (a & x > 0) ? jt : jf.
+pub const JSET = 0x40;
+
+// Miscellaneous operations/register copy.
+/// tax: x = a.
+pub const TAX = 0x00;
+/// txa: a = x.
+pub const TXA = 0x80;
+
+/// The 16 registers in the scratch memory store as named enums.
+pub const Scratch = enum(u4) { m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15 };
+pub const MEMWORDS = 16;
+pub const MAXINSNS = switch (builtin.os.tag) {
+ .linux => 4096,
+ else => 512,
+};
+pub const MINBUFSIZE = 32;
+pub const MAXBUFSIZE = 1 << 21;
+
+pub const Insn = extern struct {
+ opcode: u16,
+ jt: u8,
+ jf: u8,
+ k: u32,
+
+ /// Implements the `std.fmt.format` API.
+ /// The formatting is similar to the output of tcpdump -dd.
+ pub fn format(
+ self: Insn,
+ comptime layout: []const u8,
+ opts: std.fmt.FormatOptions,
+ writer: anytype,
+ ) !void {
+ _ = opts;
+ if (comptime layout.len != 0 and layout[0] != 's')
+ @compileError("Unsupported format specifier for BPF Insn type '" ++ layout ++ "'.");
+
+ try std.fmt.format(
+ writer,
+ "Insn{{ 0x{X:0<2}, {d}, {d}, 0x{X:0<8} }}",
+ .{ self.opcode, self.jt, self.jf, self.k },
+ );
+ }
+
+ const Size = enum(u8) {
+ word = W,
+ half_word = H,
+ byte = B,
+ };
+
+ fn stmt(opcode: u16, k: u32) Insn {
+ return .{
+ .opcode = opcode,
+ .jt = 0,
+ .jf = 0,
+ .k = k,
+ };
+ }
+
+ pub fn ld_imm(value: u32) Insn {
+ return stmt(LD | IMM, value);
+ }
+
+ pub fn ld_abs(size: Size, offset: u32) Insn {
+ return stmt(LD | ABS | @enumToInt(size), offset);
+ }
+
+ pub fn ld_ind(size: Size, offset: u32) Insn {
+ return stmt(LD | IND | @enumToInt(size), offset);
+ }
+
+ pub fn ld_mem(reg: Scratch) Insn {
+ return stmt(LD | MEM, @enumToInt(reg));
+ }
+
+ pub fn ld_len() Insn {
+ return stmt(LD | LEN | W, 0);
+ }
+
+ pub fn ld_rnd() Insn {
+ return stmt(LD | RND | W, 0);
+ }
+
+ pub fn ldx_imm(value: u32) Insn {
+ return stmt(LDX | IMM, value);
+ }
+
+ pub fn ldx_mem(reg: Scratch) Insn {
+ return stmt(LDX | MEM, @enumToInt(reg));
+ }
+
+ pub fn ldx_len() Insn {
+ return stmt(LDX | LEN | W, 0);
+ }
+
+ pub fn ldx_msh(offset: u32) Insn {
+ return stmt(LDX | MSH | B, offset);
+ }
+
+ pub fn st(reg: Scratch) Insn {
+ return stmt(ST, @enumToInt(reg));
+ }
+ pub fn stx(reg: Scratch) Insn {
+ return stmt(STX, @enumToInt(reg));
+ }
+
+ const AluOp = enum(u16) {
+ add = ADD,
+ sub = SUB,
+ mul = MUL,
+ div = DIV,
+ @"or" = OR,
+ @"and" = AND,
+ lsh = LSH,
+ rsh = RSH,
+ mod = MOD,
+ xor = XOR,
+ };
+
+ const Source = enum(u16) {
+ k = K,
+ x = X,
+ };
+ const KOrX = union(Source) {
+ k: u32,
+ x: void,
+ };
+
+ pub fn alu_neg() Insn {
+ return stmt(ALU | NEG, 0);
+ }
+
+ pub fn alu(op: AluOp, source: KOrX) Insn {
+ return stmt(
+ ALU | @enumToInt(op) | @enumToInt(source),
+ if (source == .k) source.k else 0,
+ );
+ }
+
+ const JmpOp = enum(u16) {
+ jeq = JEQ,
+ jgt = JGT,
+ jge = JGE,
+ jset = JSET,
+ };
+
+ pub fn jmp_ja(location: u32) Insn {
+ return stmt(JMP | JA, location);
+ }
+
+ pub fn jmp(op: JmpOp, source: KOrX, jt: u8, jf: u8) Insn {
+ return Insn{
+ .opcode = JMP | @enumToInt(op) | @enumToInt(source),
+ .jt = jt,
+ .jf = jf,
+ .k = if (source == .k) source.k else 0,
+ };
+ }
+
+ const Verdict = enum(u16) {
+ k = K,
+ a = A,
+ };
+ const KOrA = union(Verdict) {
+ k: u32,
+ a: void,
+ };
+
+ pub fn ret(verdict: KOrA) Insn {
+ return stmt(
+ RET | @enumToInt(verdict),
+ if (verdict == .k) verdict.k else 0,
+ );
+ }
+
+ pub fn tax() Insn {
+ return stmt(MISC | TAX, 0);
+ }
+
+ pub fn txa() Insn {
+ return stmt(MISC | TXA, 0);
+ }
+};
+
+fn opcodeEqual(opcode: u16, insn: Insn) !void {
+ try expectEqual(opcode, insn.opcode);
+}
+
+test "opcodes" {
+ try opcodeEqual(0x00, Insn.ld_imm(0));
+ try opcodeEqual(0x20, Insn.ld_abs(.word, 0));
+ try opcodeEqual(0x28, Insn.ld_abs(.half_word, 0));
+ try opcodeEqual(0x30, Insn.ld_abs(.byte, 0));
+ try opcodeEqual(0x40, Insn.ld_ind(.word, 0));
+ try opcodeEqual(0x48, Insn.ld_ind(.half_word, 0));
+ try opcodeEqual(0x50, Insn.ld_ind(.byte, 0));
+ try opcodeEqual(0x60, Insn.ld_mem(.m0));
+ try opcodeEqual(0x80, Insn.ld_len());
+ try opcodeEqual(0xc0, Insn.ld_rnd());
+
+ try opcodeEqual(0x01, Insn.ldx_imm(0));
+ try opcodeEqual(0x61, Insn.ldx_mem(.m0));
+ try opcodeEqual(0x81, Insn.ldx_len());
+ try opcodeEqual(0xb1, Insn.ldx_msh(0));
+
+ try opcodeEqual(0x02, Insn.st(.m0));
+ try opcodeEqual(0x03, Insn.stx(.m0));
+
+ try opcodeEqual(0x04, Insn.alu(.add, .{ .k = 0 }));
+ try opcodeEqual(0x14, Insn.alu(.sub, .{ .k = 0 }));
+ try opcodeEqual(0x24, Insn.alu(.mul, .{ .k = 0 }));
+ try opcodeEqual(0x34, Insn.alu(.div, .{ .k = 0 }));
+ try opcodeEqual(0x44, Insn.alu(.@"or", .{ .k = 0 }));
+ try opcodeEqual(0x54, Insn.alu(.@"and", .{ .k = 0 }));
+ try opcodeEqual(0x64, Insn.alu(.lsh, .{ .k = 0 }));
+ try opcodeEqual(0x74, Insn.alu(.rsh, .{ .k = 0 }));
+ try opcodeEqual(0x94, Insn.alu(.mod, .{ .k = 0 }));
+ try opcodeEqual(0xa4, Insn.alu(.xor, .{ .k = 0 }));
+ try opcodeEqual(0x84, Insn.alu_neg());
+ try opcodeEqual(0x0c, Insn.alu(.add, .x));
+ try opcodeEqual(0x1c, Insn.alu(.sub, .x));
+ try opcodeEqual(0x2c, Insn.alu(.mul, .x));
+ try opcodeEqual(0x3c, Insn.alu(.div, .x));
+ try opcodeEqual(0x4c, Insn.alu(.@"or", .x));
+ try opcodeEqual(0x5c, Insn.alu(.@"and", .x));
+ try opcodeEqual(0x6c, Insn.alu(.lsh, .x));
+ try opcodeEqual(0x7c, Insn.alu(.rsh, .x));
+ try opcodeEqual(0x9c, Insn.alu(.mod, .x));
+ try opcodeEqual(0xac, Insn.alu(.xor, .x));
+
+ try opcodeEqual(0x05, Insn.jmp_ja(0));
+ try opcodeEqual(0x15, Insn.jmp(.jeq, .{ .k = 0 }, 0, 0));
+ try opcodeEqual(0x25, Insn.jmp(.jgt, .{ .k = 0 }, 0, 0));
+ try opcodeEqual(0x35, Insn.jmp(.jge, .{ .k = 0 }, 0, 0));
+ try opcodeEqual(0x45, Insn.jmp(.jset, .{ .k = 0 }, 0, 0));
+ try opcodeEqual(0x1d, Insn.jmp(.jeq, .x, 0, 0));
+ try opcodeEqual(0x2d, Insn.jmp(.jgt, .x, 0, 0));
+ try opcodeEqual(0x3d, Insn.jmp(.jge, .x, 0, 0));
+ try opcodeEqual(0x4d, Insn.jmp(.jset, .x, 0, 0));
+
+ try opcodeEqual(0x06, Insn.ret(.{ .k = 0 }));
+ try opcodeEqual(0x16, Insn.ret(.a));
+
+ try opcodeEqual(0x07, Insn.tax());
+ try opcodeEqual(0x87, Insn.txa());
+}
+
+pub const Error = error{
+ InvalidOpcode,
+ InvalidOffset,
+ InvalidLocation,
+ DivisionByZero,
+ NoReturn,
+};
+
+/// A simple implementation of the BPF virtual-machine.
+/// Use this to run/debug programs.
+pub fn simulate(
+ packet: []const u8,
+ filter: []const Insn,
+ byte_order: std.builtin.Endian,
+) Error!u32 {
+ assert(filter.len > 0 and filter.len < MAXINSNS);
+ assert(packet.len < MAXBUFSIZE);
+ const len = @intCast(u32, packet.len);
+
+ var a: u32 = 0;
+ var x: u32 = 0;
+ var m = mem.zeroes([MEMWORDS]u32);
+ var pc: usize = 0;
+
+ while (pc < filter.len) : (pc += 1) {
+ const i = filter[pc];
+ // Cast to a wider type to protect against overflow.
+ const k = @as(u64, i.k);
+ const remaining = filter.len - (pc + 1);
+
+ // Do validation/error checking here to compress the second switch.
+ switch (i.opcode) {
+ LD | ABS | W => if (k + @sizeOf(u32) - 1 >= packet.len) return error.InvalidOffset,
+ LD | ABS | H => if (k + @sizeOf(u16) - 1 >= packet.len) return error.InvalidOffset,
+ LD | ABS | B => if (k >= packet.len) return error.InvalidOffset,
+ LD | IND | W => if (k + x + @sizeOf(u32) - 1 >= packet.len) return error.InvalidOffset,
+ LD | IND | H => if (k + x + @sizeOf(u16) - 1 >= packet.len) return error.InvalidOffset,
+ LD | IND | B => if (k + x >= packet.len) return error.InvalidOffset,
+
+ LDX | MSH | B => if (k >= packet.len) return error.InvalidOffset,
+ ST, STX, LD | MEM, LDX | MEM => if (i.k >= MEMWORDS) return error.InvalidOffset,
+
+ JMP | JA => if (remaining <= i.k) return error.InvalidOffset,
+ JMP | JEQ | K,
+ JMP | JGT | K,
+ JMP | JGE | K,
+ JMP | JSET | K,
+ JMP | JEQ | X,
+ JMP | JGT | X,
+ JMP | JGE | X,
+ JMP | JSET | X,
+ => if (remaining <= i.jt or remaining <= i.jf) return error.InvalidLocation,
+ else => {},
+ }
+ switch (i.opcode) {
+ LD | IMM => a = i.k,
+ LD | MEM => a = m[i.k],
+ LD | LEN | W => a = len,
+ LD | RND | W => a = random.int(u32),
+ LD | ABS | W => a = mem.readInt(u32, packet[i.k..][0..@sizeOf(u32)], byte_order),
+ LD | ABS | H => a = mem.readInt(u16, packet[i.k..][0..@sizeOf(u16)], byte_order),
+ LD | ABS | B => a = packet[i.k],
+ LD | IND | W => a = mem.readInt(u32, packet[i.k + x ..][0..@sizeOf(u32)], byte_order),
+ LD | IND | H => a = mem.readInt(u16, packet[i.k + x ..][0..@sizeOf(u16)], byte_order),
+ LD | IND | B => a = packet[i.k + x],
+
+ LDX | IMM => x = i.k,
+ LDX | MEM => x = m[i.k],
+ LDX | LEN | W => x = len,
+ LDX | MSH | B => x = @as(u32, @truncate(u4, packet[i.k])) << 2,
+
+ ST => m[i.k] = a,
+ STX => m[i.k] = x,
+
+ ALU | ADD | K => a +%= i.k,
+ ALU | SUB | K => a -%= i.k,
+ ALU | MUL | K => a *%= i.k,
+ ALU | DIV | K => a = try math.divTrunc(u32, a, i.k),
+ ALU | OR | K => a |= i.k,
+ ALU | AND | K => a &= i.k,
+ ALU | LSH | K => a = math.shl(u32, a, i.k),
+ ALU | RSH | K => a = math.shr(u32, a, i.k),
+ ALU | MOD | K => a = try math.mod(u32, a, i.k),
+ ALU | XOR | K => a ^= i.k,
+ ALU | ADD | X => a +%= x,
+ ALU | SUB | X => a -%= x,
+ ALU | MUL | X => a *%= x,
+ ALU | DIV | X => a = try math.divTrunc(u32, a, x),
+ ALU | OR | X => a |= x,
+ ALU | AND | X => a &= x,
+ ALU | LSH | X => a = math.shl(u32, a, x),
+ ALU | RSH | X => a = math.shr(u32, a, x),
+ ALU | MOD | X => a = try math.mod(u32, a, x),
+ ALU | XOR | X => a ^= x,
+ ALU | NEG => a = @bitCast(u32, -%@bitCast(i32, a)),
+
+ JMP | JA => pc += i.k,
+ JMP | JEQ | K => pc += if (a == i.k) i.jt else i.jf,
+ JMP | JGT | K => pc += if (a > i.k) i.jt else i.jf,
+ JMP | JGE | K => pc += if (a >= i.k) i.jt else i.jf,
+ JMP | JSET | K => pc += if (a & i.k > 0) i.jt else i.jf,
+ JMP | JEQ | X => pc += if (a == x) i.jt else i.jf,
+ JMP | JGT | X => pc += if (a > x) i.jt else i.jf,
+ JMP | JGE | X => pc += if (a >= x) i.jt else i.jf,
+ JMP | JSET | X => pc += if (a & x > 0) i.jt else i.jf,
+
+ RET | K => return i.k,
+ RET | A => return a,
+
+ MISC | TAX => x = a,
+ MISC | TXA => a = x,
+ else => return error.InvalidOpcode,
+ }
+ }
+
+ return error.NoReturn;
+}
+
+// This program is the BPF form of the tcpdump filter:
+//
+// tcpdump -dd 'ip host mirror.internode.on.net and tcp port ftp-data'
+//
+// As of January 2022, mirror.internode.on.net resolves to 150.101.135.3
+//
+// For reference, here's what it looks like in BPF assembler.
+// Note that the jumps are used for TCP/IP layer checks.
+//
+// ```
+// ldh [12] (#proto)
+// jeq #0x0800 (ETHERTYPE_IP), L1, fail
+// L1: ld [26]
+// jeq #150.101.135.3, L2, dest
+// dest: ld [30]
+// jeq #150.101.135.3, L2, fail
+// L2: ldb [23]
+// jeq #0x6 (IPPROTO_TCP), L3, fail
+// L3: ldh [20]
+// jset #0x1fff, fail, plen
+// plen: ldx 4 * ([14] & 0xf)
+// ldh [x + 14]
+// jeq #0x14 (FTP), pass, dstp
+// dstp: ldh [x + 16]
+// jeq #0x14 (FTP), pass, fail
+// pass: ret #0x40000
+// fail: ret #0
+// ```
+const tcpdump_filter = [_]Insn{
+ Insn.ld_abs(.half_word, 12),
+ Insn.jmp(.jeq, .{ .k = 0x800 }, 0, 14),
+ Insn.ld_abs(.word, 26),
+ Insn.jmp(.jeq, .{ .k = 0x96658703 }, 2, 0),
+ Insn.ld_abs(.word, 30),
+ Insn.jmp(.jeq, .{ .k = 0x96658703 }, 0, 10),
+ Insn.ld_abs(.byte, 23),
+ Insn.jmp(.jeq, .{ .k = 0x6 }, 0, 8),
+ Insn.ld_abs(.half_word, 20),
+ Insn.jmp(.jset, .{ .k = 0x1fff }, 6, 0),
+ Insn.ldx_msh(14),
+ Insn.ld_ind(.half_word, 14),
+ Insn.jmp(.jeq, .{ .k = 0x14 }, 2, 0),
+ Insn.ld_ind(.half_word, 16),
+ Insn.jmp(.jeq, .{ .k = 0x14 }, 0, 1),
+ Insn.ret(.{ .k = 0x40000 }),
+ Insn.ret(.{ .k = 0 }),
+};
+
+// This packet is the output of `ls` on mirror.internode.on.net:/, captured
+// using the filter above.
+//
+// zig fmt: off
+const ftp_data = [_]u8{
+ // ethernet - 14 bytes: IPv4(0x0800) from a4:71:74:ad:4b:f0 -> de:ad:be:ef:f0:0f
+ 0xde, 0xad, 0xbe, 0xef, 0xf0, 0x0f, 0xa4, 0x71, 0x74, 0xad, 0x4b, 0xf0, 0x08, 0x00,
+ // IPv4 - 20 bytes: TCP data from 150.101.135.3 -> 192.168.1.3
+ 0x45, 0x00, 0x01, 0xf2, 0x70, 0x3b, 0x40, 0x00, 0x37, 0x06, 0xf2, 0xb6,
+ 0x96, 0x65, 0x87, 0x03, 0xc0, 0xa8, 0x01, 0x03,
+ // TCP - 32 bytes: Source port: 20 (FTP). Payload = 446 bytes
+ 0x00, 0x14, 0x80, 0x6d, 0x35, 0x81, 0x2d, 0x40, 0x4f, 0x8a, 0x29, 0x9e, 0x80, 0x18, 0x00, 0x2e,
+ 0x88, 0x8d, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0x0b, 0x59, 0x5d, 0x09, 0x32, 0x8b, 0x51, 0xa0
+} ++
+ // Raw line-based FTP data - 446 bytes
+ "lrwxrwxrwx 1 root root 12 Feb 14 2012 debian -> .pub2/debian\r\n" ++
+ "lrwxrwxrwx 1 root root 15 Feb 14 2012 debian-cd -> .pub2/debian-cd\r\n" ++
+ "lrwxrwxrwx 1 root root 9 Mar 9 2018 linux -> pub/linux\r\n" ++
+ "drwxr-xr-X 3 mirror mirror 4096 Sep 20 08:10 pub\r\n" ++
+ "lrwxrwxrwx 1 root root 12 Feb 14 2012 ubuntu -> .pub2/ubuntu\r\n" ++
+ "-rw-r--r-- 1 root root 1044 Jan 20 2015 welcome.msg\r\n";
+// zig fmt: on
+
+test "tcpdump filter" {
+ try expectEqual(
+ @as(u32, 0x40000),
+ try simulate(ftp_data, &tcpdump_filter, .Big),
+ );
+}
+
+fn expectPass(data: anytype, filter: []Insn) !void {
+ try expectEqual(
+ @as(u32, 0),
+ try simulate(mem.asBytes(data), filter, .Big),
+ );
+}
+
+fn expectFail(expected_error: anyerror, data: anytype, filter: []Insn) !void {
+ try expectError(
+ expected_error,
+ simulate(mem.asBytes(data), filter, native_endian),
+ );
+}
+
+test "simulator coverage" {
+ const some_data: packed struct {
+ foo: u32,
+ bar: u8,
+ } = .{
+ .foo = mem.nativeToBig(u32, 0xaabbccdd),
+ .bar = 0x7f,
+ };
+
+ try expectPass(&some_data, &.{
+ // ld #10
+ // ldx #1
+ // st M[0]
+ // stx M[1]
+ // fail if A != 10
+ Insn.ld_imm(10),
+ Insn.ldx_imm(1),
+ Insn.st(.m0),
+ Insn.stx(.m1),
+ Insn.jmp(.jeq, .{ .k = 10 }, 1, 0),
+ Insn.ret(.{ .k = 1 }),
+ // ld [0]
+ // fail if A != 0xaabbccdd
+ Insn.ld_abs(.word, 0),
+ Insn.jmp(.jeq, .{ .k = 0xaabbccdd }, 1, 0),
+ Insn.ret(.{ .k = 2 }),
+ // ldh [0]
+ // fail if A != 0xaabb
+ Insn.ld_abs(.half_word, 0),
+ Insn.jmp(.jeq, .{ .k = 0xaabb }, 1, 0),
+ Insn.ret(.{ .k = 3 }),
+ // ldb [0]
+ // fail if A != 0xaa
+ Insn.ld_abs(.byte, 0),
+ Insn.jmp(.jeq, .{ .k = 0xaa }, 1, 0),
+ Insn.ret(.{ .k = 4 }),
+ // ld [x + 0]
+ // fail if A != 0xbbccdd7f
+ Insn.ld_ind(.word, 0),
+ Insn.jmp(.jeq, .{ .k = 0xbbccdd7f }, 1, 0),
+ Insn.ret(.{ .k = 5 }),
+ // ldh [x + 0]
+ // fail if A != 0xbbcc
+ Insn.ld_ind(.half_word, 0),
+ Insn.jmp(.jeq, .{ .k = 0xbbcc }, 1, 0),
+ Insn.ret(.{ .k = 6 }),
+ // ldb [x + 0]
+ // fail if A != 0xbb
+ Insn.ld_ind(.byte, 0),
+ Insn.jmp(.jeq, .{ .k = 0xbb }, 1, 0),
+ Insn.ret(.{ .k = 7 }),
+ // ld M[0]
+ // fail if A != 10
+ Insn.ld_mem(.m0),
+ Insn.jmp(.jeq, .{ .k = 10 }, 1, 0),
+ Insn.ret(.{ .k = 8 }),
+ // ld #len
+ // fail if A != 5
+ Insn.ld_len(),
+ Insn.jmp(.jeq, .{ .k = @sizeOf(@TypeOf(some_data)) }, 1, 0),
+ Insn.ret(.{ .k = 9 }),
+ // ld #0
+ // ld arc4random()
+ // fail if A == 0
+ Insn.ld_imm(0),
+ Insn.ld_rnd(),
+ Insn.jmp(.jgt, .{ .k = 0 }, 1, 0),
+ Insn.ret(.{ .k = 10 }),
+ // ld #3
+ // ldx #10
+ // st M[2]
+ // txa
+ // fail if a != x
+ Insn.ld_imm(3),
+ Insn.ldx_imm(10),
+ Insn.st(.m2),
+ Insn.txa(),
+ Insn.jmp(.jeq, .x, 1, 0),
+ Insn.ret(.{ .k = 11 }),
+ // ldx M[2]
+ // fail if A <= X
+ Insn.ldx_mem(.m2),
+ Insn.jmp(.jgt, .x, 1, 0),
+ Insn.ret(.{ .k = 12 }),
+ // ldx #len
+ // fail if a <= x
+ Insn.ldx_len(),
+ Insn.jmp(.jgt, .x, 1, 0),
+ Insn.ret(.{ .k = 13 }),
+ // a = 4 * (0x7f & 0xf)
+ // x = 4 * ([4] & 0xf)
+ // fail if a != x
+ Insn.ld_imm(4 * (0x7f & 0xf)),
+ Insn.ldx_msh(4),
+ Insn.jmp(.jeq, .x, 1, 0),
+ Insn.ret(.{ .k = 14 }),
+ // ld #(u32)-1
+ // ldx #2
+ // add #1
+ // fail if a != 0
+ Insn.ld_imm(0xffffffff),
+ Insn.ldx_imm(2),
+ Insn.alu(.add, .{ .k = 1 }),
+ Insn.jmp(.jeq, .{ .k = 0 }, 1, 0),
+ Insn.ret(.{ .k = 15 }),
+ // sub #1
+ // fail if a != (u32)-1
+ Insn.alu(.sub, .{ .k = 1 }),
+ Insn.jmp(.jeq, .{ .k = 0xffffffff }, 1, 0),
+ Insn.ret(.{ .k = 16 }),
+ // add x
+ // fail if a != 1
+ Insn.alu(.add, .x),
+ Insn.jmp(.jeq, .{ .k = 1 }, 1, 0),
+ Insn.ret(.{ .k = 17 }),
+ // sub x
+ // fail if a != (u32)-1
+ Insn.alu(.sub, .x),
+ Insn.jmp(.jeq, .{ .k = 0xffffffff }, 1, 0),
+ Insn.ret(.{ .k = 18 }),
+ // ld #16
+ // mul #2
+ // fail if a != 32
+ Insn.ld_imm(16),
+ Insn.alu(.mul, .{ .k = 2 }),
+ Insn.jmp(.jeq, .{ .k = 32 }, 1, 0),
+ Insn.ret(.{ .k = 19 }),
+ // mul x
+ // fail if a != 64
+ Insn.alu(.mul, .x),
+ Insn.jmp(.jeq, .{ .k = 64 }, 1, 0),
+ Insn.ret(.{ .k = 20 }),
+ // div #2
+ // fail if a != 32
+ Insn.alu(.div, .{ .k = 2 }),
+ Insn.jmp(.jeq, .{ .k = 32 }, 1, 0),
+ Insn.ret(.{ .k = 21 }),
+ // div x
+ // fail if a != 16
+ Insn.alu(.div, .x),
+ Insn.jmp(.jeq, .{ .k = 16 }, 1, 0),
+ Insn.ret(.{ .k = 22 }),
+ // or #4
+ // fail if a != 20
+ Insn.alu(.@"or", .{ .k = 4 }),
+ Insn.jmp(.jeq, .{ .k = 20 }, 1, 0),
+ Insn.ret(.{ .k = 23 }),
+ // or x
+ // fail if a != 22
+ Insn.alu(.@"or", .x),
+ Insn.jmp(.jeq, .{ .k = 22 }, 1, 0),
+ Insn.ret(.{ .k = 24 }),
+ // and #6
+ // fail if a != 6
+ Insn.alu(.@"and", .{ .k = 0b110 }),
+ Insn.jmp(.jeq, .{ .k = 6 }, 1, 0),
+ Insn.ret(.{ .k = 25 }),
+ // and x
+ // fail if a != 2
+ Insn.alu(.@"and", .x),
+ Insn.jmp(.jeq, .x, 1, 0),
+ Insn.ret(.{ .k = 26 }),
+ // xor #15
+ // fail if a != 13
+ Insn.alu(.xor, .{ .k = 0b1111 }),
+ Insn.jmp(.jeq, .{ .k = 0b1101 }, 1, 0),
+ Insn.ret(.{ .k = 27 }),
+ // xor x
+ // fail if a != 15
+ Insn.alu(.xor, .x),
+ Insn.jmp(.jeq, .{ .k = 0b1111 }, 1, 0),
+ Insn.ret(.{ .k = 28 }),
+ // rsh #1
+ // fail if a != 7
+ Insn.alu(.rsh, .{ .k = 1 }),
+ Insn.jmp(.jeq, .{ .k = 0b0111 }, 1, 0),
+ Insn.ret(.{ .k = 29 }),
+ // rsh x
+ // fail if a != 1
+ Insn.alu(.rsh, .x),
+ Insn.jmp(.jeq, .{ .k = 0b0001 }, 1, 0),
+ Insn.ret(.{ .k = 30 }),
+ // lsh #1
+ // fail if a != 2
+ Insn.alu(.lsh, .{ .k = 1 }),
+ Insn.jmp(.jeq, .{ .k = 0b0010 }, 1, 0),
+ Insn.ret(.{ .k = 31 }),
+ // lsh x
+ // fail if a != 8
+ Insn.alu(.lsh, .x),
+ Insn.jmp(.jeq, .{ .k = 0b1000 }, 1, 0),
+ Insn.ret(.{ .k = 32 }),
+ // mod 6
+ // fail if a != 2
+ Insn.alu(.mod, .{ .k = 6 }),
+ Insn.jmp(.jeq, .{ .k = 2 }, 1, 0),
+ Insn.ret(.{ .k = 33 }),
+ // mod x
+ // fail if a != 0
+ Insn.alu(.mod, .x),
+ Insn.jmp(.jeq, .{ .k = 0 }, 1, 0),
+ Insn.ret(.{ .k = 34 }),
+ // tax
+ // neg
+ // fail if a != (u32)-2
+ Insn.txa(),
+ Insn.alu_neg(),
+ Insn.jmp(.jeq, .{ .k = ~@as(u32, 2) + 1 }, 1, 0),
+ Insn.ret(.{ .k = 35 }),
+ // ja #1 (skip the next instruction)
+ Insn.jmp_ja(1),
+ Insn.ret(.{ .k = 36 }),
+ // ld #20
+ // tax
+ // fail if a != 20
+ // fail if a != x
+ Insn.ld_imm(20),
+ Insn.tax(),
+ Insn.jmp(.jeq, .{ .k = 20 }, 1, 0),
+ Insn.ret(.{ .k = 37 }),
+ Insn.jmp(.jeq, .x, 1, 0),
+ Insn.ret(.{ .k = 38 }),
+ // ld #19
+ // fail if a == 20
+ // fail if a == x
+ // fail if a >= 20
+ // fail if a >= X
+ Insn.ld_imm(19),
+ Insn.jmp(.jeq, .{ .k = 20 }, 0, 1),
+ Insn.ret(.{ .k = 39 }),
+ Insn.jmp(.jeq, .x, 0, 1),
+ Insn.ret(.{ .k = 40 }),
+ Insn.jmp(.jgt, .{ .k = 20 }, 0, 1),
+ Insn.ret(.{ .k = 41 }),
+ Insn.jmp(.jgt, .x, 0, 1),
+ Insn.ret(.{ .k = 42 }),
+ // ld #21
+ // fail if a < 20
+ // fail if a < x
+ Insn.ld_imm(21),
+ Insn.jmp(.jgt, .{ .k = 20 }, 1, 0),
+ Insn.ret(.{ .k = 43 }),
+ Insn.jmp(.jgt, .x, 1, 0),
+ Insn.ret(.{ .k = 44 }),
+ // ldx #22
+ // fail if a < 22
+ // fail if a < x
+ Insn.ldx_imm(22),
+ Insn.jmp(.jge, .{ .k = 22 }, 0, 1),
+ Insn.ret(.{ .k = 45 }),
+ Insn.jmp(.jge, .x, 0, 1),
+ Insn.ret(.{ .k = 46 }),
+ // ld #23
+ // fail if a >= 22
+ // fail if a >= x
+ Insn.ld_imm(23),
+ Insn.jmp(.jge, .{ .k = 22 }, 1, 0),
+ Insn.ret(.{ .k = 47 }),
+ Insn.jmp(.jge, .x, 1, 0),
+ Insn.ret(.{ .k = 48 }),
+ // ldx #0b10100
+ // fail if a & 0b10100 == 0
+ // fail if a & x == 0
+ Insn.ldx_imm(0b10100),
+ Insn.jmp(.jset, .{ .k = 0b10100 }, 1, 0),
+ Insn.ret(.{ .k = 47 }),
+ Insn.jmp(.jset, .x, 1, 0),
+ Insn.ret(.{ .k = 48 }),
+ // ldx #0
+ // fail if a & 0 > 0
+ // fail if a & x > 0
+ Insn.ldx_imm(0),
+ Insn.jmp(.jset, .{ .k = 0 }, 0, 1),
+ Insn.ret(.{ .k = 49 }),
+ Insn.jmp(.jset, .x, 0, 1),
+ Insn.ret(.{ .k = 50 }),
+ Insn.ret(.{ .k = 0 }),
+ });
+ try expectPass(&some_data, &.{
+ Insn.ld_imm(35),
+ Insn.ld_imm(0),
+ Insn.ret(.a),
+ });
+
+ // Errors
+ try expectFail(error.NoReturn, &some_data, &.{
+ Insn.ld_imm(10),
+ });
+ try expectFail(error.InvalidOpcode, &some_data, &.{
+ Insn.stmt(0x7f, 0xdeadbeef),
+ });
+ try expectFail(error.InvalidOffset, &some_data, &.{
+ Insn.stmt(LD | ABS | W, 10),
+ });
+ try expectFail(error.InvalidLocation, &some_data, &.{
+ Insn.jmp(.jeq, .{ .k = 0 }, 10, 0),
+ });
+ try expectFail(error.InvalidLocation, &some_data, &.{
+ Insn.jmp(.jeq, .{ .k = 0 }, 0, 10),
+ });
+}