The following penguin plugins are currently supported. Each is documented below.
- Coverage: Track block-level coverage of binaries
- Env: Track usage of boot arguments, environment variable accesses, and environment variable comparisons.
- Health: Track system health metrics including processes run.
- Interfaces: Track network interfaces referenced
- KernelVersion: Change output of uname syscall
- Lifeguard: Track and block signals
- Mounts: Track attempts to mount file systems
- NVRAM2: Tracks accesses to NVRAM
- Netbinds: Track network listening guest processes
- Nmap: Network scanning for guest applications that bind to TCP ports
- Pseudofiles: Model and monitor interactions to devices in
/dev/procand/sys - Shell: Track behavior of shell scripts including lines executed
- VPNguin: Bridge network connections to networked guest processes
- Zap: Currently disabled Network scanning of guest web applications
This plugin tracks the module and offset block level coverage of all binaries
in the system. These results are reported in coverage.csv.
The file coverage_tree.csv stores this information with parent/child
relationships to visualize as a tree. The file coverage_transitions.csv
records all context switches between processes.
The crashes plugin records fatal signal deliveries to guest processes
(userland crashes) in crashes.yaml. Each record includes the process name,
pid, signal number and name, the faulting program counter, the time (host
wall-clock seconds since emulation start) of the first occurrence, and a
count de-duplicating identical (proc, signal, pc) deliveries. By default
it records SIGSEGV, SIGBUS, SIGILL, SIGABRT, SIGFPE, and SIGSYS; the set is
configurable via the signals argument (use signal names — numbering is
resolved per guest architecture). The number of unique crash records is also
reported (negated) as the crashes score category.
Note that records are signal deliveries, not confirmed terminations: the
driver hook fires when a signal is dequeued for delivery, before the kernel
applies its disposition, and the event does not indicate whether the process
has a handler installed. A process that catches a fatal-class signal and
survives is still recorded. Deliveries that another plugin has already
dropped (event.drop, e.g. a SIGILL-bypass consumer) are not recorded.
The env plugin dynamically tracks linux environment variables accessed through
/proc/cmdline and calls to getenv. It also tracks accesses to /proc/mtd
as well as /dev/mtdX to identify accesses to u-boot environment variables.
If an env value is set to the magic string DYNVALDYNVALDYNVAL a dynamic analysis
to detect comparisons between this magic string and any other string will be enabled.
The results of this analysis will be stored in env_cmp.txt
In a config file, a user may add key-value pairs into the env filed to set new
values into the linux environment. Note that a number of required internal variables
(e.g., root=/dev/vda) will added to the system's arguments after any arguments you specify here.
The health plugin tracks the system health over time. health.csv tracks counts
of various behaviors of interest over time while health_final.yaml just reports
these values at the end of execution.
The plugin also creates health_procs.txt as a sorted list of processes run and
health_procs_with_args.txt as a sorted list of processes with their arguments.
"Easy mode" instrumentation: register uprobes and syscall hooks from
printf-style action strings (sys_read(%fd, %s:out, %d) = %d) instead of
writing callbacks. Drive it from the breakpoint CLI (over the RemoteCtrl
plugin's socket) or the Python API. The grammar, format specifiers and filters
are in hooklogger.md.
Track network interfaces referenced in executed commands. Results are
reported in iface.log.
Change output of uname syscall. Config Options:
sysname: changes operating system namenodename: changtes network node hostnamerelease: changes the kernel releasekversion: changes the kernel versionmachine: changes the machine hardware namedomainname: changes NIS or YP domain name
Track and block signals sent between processes. Results are stored in
lifeguard.csv.
Lifeguard suppresses configured signals sent through supported signal-send
syscalls by skipping the syscall before the kernel sends the signal. Supported
syscalls are defined in one table in the plugin and resolved through the syscall
prototype argument helpers supplied by the driver, falling back to fixed indexes
only if the prototype names are unavailable. It also consumes signal_monitor
for configured signals other than SIGKILL and SIGSTOP, allowing delivery
paths to be observed and dropped when the driver hook is effective.
Delivery-time drops are not equivalent to preventing a signal from being sent.
The driver hooks the kernel dequeue path, so it can drop catchable signal
deliveries after the kernel has selected a target. Default-fatal signals that do
not have a userspace handler may start process or thread-group exit before that
dequeue path is reached. For process-preserving behavior, the preferred path is
still to block the sending syscall before the kernel applies signal semantics.
SIGKILL and SIGSTOP cannot be caught or ignored by Linux processes, so
Lifeguard treats them as syscall-only and only suppresses instances generated by
supported signal-send syscalls.
The syscall path covers the supported signal-send syscalls currently listed by
Lifeguard: kill, tkill, tgkill, rt_sigqueueinfo,
rt_tgsigqueueinfo, and pidfd_send_signal. Signals sent by other kernel paths
or unsupported syscalls may be visible through delivery monitoring when the
driver hooks them, but the syscall fallback will not prevent them before send
time.
Some signals also require guest state repair when dropped. For example, blindly
dropping synchronous fault signals such as SIGILL, SIGSEGV, SIGBUS,
SIGFPE, SIGTRAP, or SIGSYS can immediately re-enter the same faulting
instruction. Use a purpose-built signal_monitor consumer, such as
sigill_bypass.py, when the handler needs to advance the PC, emulate an
instruction, or otherwise repair guest state.
Track which file systems are mounted (or attempted to be mounted) at which paths.
Results stored in mounts.csv. Note this plugin will track some penguin-internal
initialization logic with mounts in the /igloo directory.
This plugin tracks accesses to keys and values stored in NVRAM. Results
are stored in nvram.csv
This plugin detects and logs network binds by guest processes. The results
are logged into netbinds.csv and include a time column indicating how
many seconds after boot until the bind occurred, plus state and closed_time
columns describing each socket's final lifecycle state:
listening— announced and still bound.transient— released within the announce window; not a working service.closed— listened, then released for good (closed_timeset).pending— bound when the run ended but younger than the announce window; never confirmed, so not a working service either.
New binds are announced to downstream plugins (VPN, Nmap, FetchWeb) only after
an announce debounce (announce_debounce_s, default 2s), so short-lived binds
never trigger VPN bridging or scans. Both the announce and the close/flap
debounce windows use host wall-clock time, so the classification of a
very short-lived bind can vary with emulation speed. Every open/flap/
transient/close transition is appended to netbind_events.csv, and a
per-socket lifecycle summary is written to netbinds_lifecycle.csv at run end.
The socket lifecycle survives a VM snapshot/restore: the record is bundled with
the snapshot and rebuilt on restore (a restored guest is already past its binds
and would otherwise vanish from netbinds.csv). Already-announced services are
rebuilt silently — downstream consumers replay their own state — while a service
still within its announce window at snapshot time is announced on restore.
This plugin runs nmap scans on all network-listening services.
It depends on the VPN plugin to establish network connections to guest services.
Logs are written to nmap_{protocol}_{port}.log
This plugin tracks accesses and interactions with files in /dev/ and /proc/.
In pseudofiles_failures.yaml details of failed interactions are reported,
ranked by impact (crashing_callers >> distinct_callers > hits), with the
observed calling processes and a suggested starting model per path that can be
pasted directly into the pseudofiles config section.
Users can add pseudofiles and configure models for reads, writes, and IOCTLs on
these files by adding entries into the pseudofiles config section. See the
pseudofile model catalog for every available model
(read/write/ioctl/poll/seek/mmap/open/release), copy-paste snippets, and how to
add custom models in Python via @register_model.
This plugin tracks the behavior of shell scripts, capturing coverage in shell_cov.csv, environment variable values in shell_env.csv and a combined trace in shell_cov_trace.csv.
This plugin detects network binds and configures a custom VPN to bridge
network connections to guest services. The mappings between guest
network services and what port the VPN exposes them on are listed
in vpn_bridges.csv For example, if the file contains:
procname,ipvn,domain,guest_ip,guest_port,host_port
lighttpd,ipv4,tcp,127.0.0.1,80,80
lighttpd,ipv4,tcp,192.168.0.1,80,48823
This means lighttpd started listening on port 80 on the loopback interface as well as another IP address.
To talk to the service as if you were connecting via loopback, you'd connect to the relevant host_port, here 80.
To talk to the service as if you were connecting via the other IP address, you'd connect to the other host_port, here 48823.
Note these are ports within your container, not on your host, so you must connect to the appropriate IP address to reach
the container.
The plugin also writes two convenience files in the results directory,
regenerated as bridges are established: guest_ip (the bare reachable IP, same
content as ipaddr.txt) and endpoints.txt (one
proto host_ip:host_port -> guest_ip:guest_port (proc) line per bridge).
host_port matches guest_port when it can, but the plugin has to move it when
that port is unavailable — already bound in the container, already claimed by an
earlier bind in this same run, or privileged (below 1024) when the process
cannot bind privileged ports. It then searches deterministically in strides of
1000: guest port 80 becomes 1080, or 2080 if 1080 is taken too, and so on.
So the same guest port can be published on different host ports in different
runs of the same container, without anything having changed in the config. That
is working as intended, but it means nothing may hardcode a host port. Read
it from endpoints.txt (or the host_port column of netbinds.csv), which is
the only source of truth for a given run. The run log also states the mapping
and the reason for it as each bind happens:
lighttpd binds tcp 0.0.0.0:80 reach it at container foo:2080 80 is already in use
An explicit mapping is available when you need a fixed port: set
plugins.vpn.IGLOO_VPN_PORT_MAPS (or the IGLOO_VPN_PORT_MAPS environment
variable) to comma-separated <proto>:<host_port>:<guest_ip>:<guest_port>
rules, e.g. tcp:8080:192.168.0.1:80. A fixed mapping is never relocated: if
the host port is busy the run fails loudly instead.
Every run also gets an executable results/<n>/connect.sh, generated by
penguin (not the VPN plugin) next to runtime.yaml. It is the one-glance "get
me into this device" helper and talks to the guest's serial root shell
through the container's localhost
(docker exec <container> telnet localhost <port>), so it does not depend
on the container's per-run IP address:
-
./connect.sh— attach to the serial console. -
./connect.sh <cmd...>— run a command on the serial console, print its output, and exit with the command's own status. Waits up toPENGUIN_CONNECT_TIMEOUTseconds (default 15) for the guest to answer, and exits 1 with a diagnostic if it does not — so it is safe in a script:if ./connect.sh "test -f /var/run/httpd.pid"; then echo "httpd is up"; fi
It requires core.root_shell: true (there is no serial console otherwise) and
prints how to enable it when it is off.
Currently disabled
This plugin runs the zap web application scanner to crawl and interact with guest
web applications listening on TCP port 80. Logs are written to zap.log and zap_tcp_80.log.