Giter Club home page Giter Club logo

hubris's Introduction

Hubris

dist

Hubris is a microcontroller operating environment designed for deeply-embedded systems with reliability requirements. Its design was initially proposed in RFD41, but has evolved considerably since then.

Learning

Developer documentation is in Asciidoc in the doc/ directory. It gets rendered via GitHub pages, and is available at https://oxidecomputer.github.io/hubris .

Navigating

The repo is laid out as follows.

  • app/ is where the top-level binary crates for applications live, e.g. app/gimlet contains the firmware crate for Gimlet. Generally speaking, if you want to build an image for something, look here.

  • build/ contains the build system and supporting crates.

  • chip/ contains peripheral definitions and debugging support files for individual microcontrollers.

  • doc/ contains developer documentation.

  • drv/ contains drivers, a mix of simple driver lib crates and fully-fledged server bin crates. Current convention is that drv/SYSTEM-DEVICE is the driver for DEVICE on SYSTEM (where SYSTEM is usually an SoC name), whereas drv/SYSTEM-DEVICE-server is the server bin crate.

  • idl/ contains interface definitions written in Idol

  • lib/ contains assorted utility libraries we've written. If you need to make a reusable crate that doesn't fit into one of the other directories, it probably belongs here.

  • stage0/ is the bootloader/hypovisor, primarily for LPC55.

  • support/ contains some interface and programming support files, like fake certificates and programmer firmware images.

  • sys/ contains the "system" bits of Hubris, namely the kernel (sys/kern), the shared crate defining the ABI (sys/abi), and the user library used by tasks (sys/userlib).

  • task/ contains reusable tasks that aren't drivers. The distinction between things that live in task vs in drv/something-server is fuzzy. Use your judgement.

  • test/ contains the test framework and binary crates for building it for various boards.

  • website/ contains the source code for the hubris website

Developing

We currently support Linux and Windows as first-tier platforms. macOS is also used on a daily basis by Oxide employees, but is not tested in CI. The build probably also works on Illumos; if anyone would like to step up to maintain support and a continuous build for Illumos or macOS, we'd love the help.

To submit changes for review, push them to a branch in a fork and submit a pull request to merge that branch into master. For details, see CONTRIBUTING.md.

Prereqs

You will need:

  • A rustup-based toolchain install. rustup will take care of automatically installing our pinned toolchain version, and the cross-compilation targets, when you first try to build.

  • openocd (ideally 0.11) or (if using the LPC55) pyocd (0.27 or later). Note that the 0.10 release of OpenOCD predates the STLink v3. People are using various post-0.10, pre-0.11 builds provided by system package managers, with some success, but if your system isn't packaging 0.11 yet, pester them. If you're going to use Homebrew on macOS to install OpenOCD, you need to use brew install --head openocd to build the tip of the main branch rather than using the latest binary release. If you need to build from source, you can find OpenOCD v0.11.0 here. When running ./configure, make sure that you see that the ST-Link Programmer is set enabled (which should be the default).

  • libusb, typically found from your system's package manager as libusb-1.0.0 or similar.

  • libfdti1, found as libftdi1-dev or similar.

  • If you will be running GDB, you should install arm-none-eabi-gdb. This is typically from your system's package manager with a package name like arm-none-eabi-gdb or gdb-multiarch. macOS users can run brew install --cask gcc-arm-embedded to install the official ARM binaries.

  • The Hubris debugger, Humility. Note that cargo install interacts strangely with the rust-toolchain.toml file present in the root of this repository; if you run the following command verbatim to install Humility, do so from a different directory:

    • cargo install --git https://github.com/oxidecomputer/humility.git --locked humility
      • Requires cargo-readme as a dependency: cargo install cargo-readme

Windows

There are three alternative ways to install OpenOCD:

See here for getting the source of openocd or get unofficial binaries.

Alternatively, you can install with chocolatey:

> choco install openocd

Lastly, you could install openocd with scoop:

> scoop bucket add extras
> scoop install openocd

Note: openocd installed via scoop has proven problematic for some users. If you experience problems, try installing via choco or from source (see above).

To use the ST-Link programmer, you'll probably need to install this driver.

It's not necessary to build and run Hubris, but if you want to communicate over a serial link (and that's not supported by your terminal), you'll want to use PuTTY; this guide does a good job of explaining how.

Build

We do not use cargo build or cargo run directly because they are too inflexible for our purposes. We have a complex multi-architecture build, which is a bit beyond them.

Instead, the repo includes a Cargo extension called xtask that namespaces our custom build commands.

cargo xtask dist TOMLFILE builds a distribution image for the application described by the TOML file.

  • cargo xtask dist app/demo-stm32f4-discovery/app.toml - stm32f4-discovery
  • cargo xtask dist app/demo-stm32f4-discovery/app-f3.toml - stm32f3-discovery
  • cargo xtask dist app/lpc55xpresso/app.toml - lpcxpresso55s69
  • cargo xtask dist app/demo-stm32g0-nucleo/app-g031.toml - stm32g031
  • cargo xtask dist app/demo-stm32g0-nucleo/app-g070.toml - stm32g070
  • cargo xtask dist app/demo-stm32g0-nucleo/app-g0b1.toml - stm32g0b1
  • cargo xtask dist app/demo-stm32h7-nucleo/app-h743.toml - nucleo-ih743zi2
  • cargo xtask dist app/demo-stm32h7-nucleo/app-h753.toml - nucleo-ih753zi
  • cargo xtask dist app/gemini-bu/app.toml - Gemini bringup board

Iterating

Because a full image build can take 10 seconds or more, depending on what you've changed, when you're iterating on a task or kernel you'll probably want to build it separately. This is what cargo xtask build is for.

For instance, to build task-ping as it would be built in one of the images, but without building the rest of the demo, run:

$ cargo xtask build app/gimletlet/app.toml ping

Running clippy

The cargo xtask clippy subcommand can be used to run clippy against one or more tasks in the context of a particular image:

$ cargo xtask clippy app/gimletlet/app.toml ping pong

Integrating with rust-analyzer

The Hubris build system will not work with rust-analyzer out of the box.

However, cargo xtask lsp is here to help: it takes as its argument a Rust file, and returns JSON-encoded configuration for how to set up rust-analyzer.

To use this data, some editor configuration is required!

(we haven't made plugins yet, but it would certainly be possible)

Using Neovim and rust-tools, here's an example configuration:

-- monkeypatch rust-tools to correctly detect our custom rust-analyzer
require'rust-tools.utils.utils'.is_ra_server = function (client)
  local name = client.name
  local target = "rust_analyzer"
  return string.sub(client.name, 1, string.len(target)) == target
    or client.name == "rust_analyzer-standalone"
end

-- Configure LSP through rust-tools.nvim plugin, with lots of bonus
-- content for Hubris compatibility
local cache = {}
local clients = {}
require'rust-tools'.setup{
  tools = { -- rust-tools options
    autoSetHints = true,
    inlay_hints = {
      show_parameter_hints = false,
      parameter_hints_prefix = "",
      other_hints_prefix = "",
      -- do other configuration here as desired
    },
  },

  server = {
    on_new_config = function(new_config, new_root_dir)
      local bufnr = vim.api.nvim_get_current_buf()
      local bufname = vim.api.nvim_buf_get_name(bufnr)
      local dir = new_config.root_dir()
      if string.find(dir, "hubris") then
        -- Run `xtask lsp` for the target file, which gives us a JSON
        -- dictionary with bonus configuration.
        local prev_cwd = vim.fn.getcwd()
        vim.cmd("cd " .. dir)
        local cmd = dir .. "/target/debug/xtask lsp "
        -- Notify `xtask lsp` of existing clients in the CLI invocation,
        -- so it can check against them first (which would mean a faster
        -- attach)
        for _,v in pairs(clients) do
          local c = vim.fn.escape(vim.json.encode(v), '"')
          cmd = cmd .. '-c"' .. c .. '" '
        end
        local handle = io.popen(cmd .. bufname)
        handle:flush()
        local result = handle:read("*a")
        handle:close()
        vim.cmd("cd " .. prev_cwd)

        -- If `xtask` doesn't know about `lsp`, then it will print an error to
        -- stderr and return nothing on stdout.
        if result == "" then
          vim.notify("recompile `xtask` for `lsp` support", vim.log.levels.WARN)
        end

        -- If the given file should be handled with special care, then
        -- we give the rust-analyzer client a custom name (to prevent
        -- multiple buffers from attaching to it), then cache the JSON in
        -- a local variable for use in `on_attach`
        local json = vim.json.decode(result)
        if json["Ok"] ~= nil then
          new_config.name = "rust_analyzer_" .. json.Ok.hash
          cache[bufnr] = json
          table.insert(clients, {toml = json.Ok.app, task = json.Ok.task})
        else
          -- TODO:
          -- vim.notify(vim.inspect(json.Err), vim.log.levels.ERROR)
        end
      end
    end,

    on_attach = function(client, bufnr)
      local json = cache[bufnr]
      if json ~= nil then
        local config = vim.deepcopy(client.config)
        local ra = config.settings["rust-analyzer"]
        -- Do rust-analyzer builds in a separate folder to avoid blocking
        -- the main build with a file lock.
        table.insert(json.Ok.buildOverrideCommand, "--target-dir")
        table.insert(json.Ok.buildOverrideCommand, "target/rust-analyzer")
        ra.cargo = {
          extraEnv = json.Ok.extraEnv,
          features = json.Ok.features,
          noDefaultFeatures = true,
          target = json.Ok.target,
          buildScripts = {
            overrideCommand = json.Ok.buildOverrideCommand,
          },
        }
        ra.check = {
          overrideCommand = json.Ok.buildOverrideCommand,
        }
        config.lspinfo = function()
          return { "Hubris app:      " .. json.Ok.app,
                   "Hubris task:     " .. json.Ok.task }
        end
        client.config = config
      end
    end,

    settings = {
      ["rust-analyzer"] = {
        -- enable clippy on save
        checkOnSave = {
          command = "clippy",
          extraArgs = { '--target-dir', 'target/rust-analyzer' },
        },
        diagnostics = {
          disabled = {"inactive-code"},
        },
      }
    }
  },
}
end

What's going on here?

When a new LSP configuration is created (on_new_config), we run cargo xtask lsp on the target file. The JSON configuration includes a hash of the configuration; we use that hash to modify the name of the client from rust_analyzer to rust_analyzer_$HASH. This prevents Neovim from attempting to reuse an existing client, which are normally deduplicated by client name and workspace root directory; in Hubris, we want multiple clients coexisting with same workspace root, so they need different names. Then, we stash the rest of the configuration in a local variable (cache), and record the existence of this client in clients.

When attaching to the LSP, we try to pull the configuration out of cache. If one exists, then we know we're dealing with a Hubris buffer; copy over relevant portions of the configuration.

Note that this does not compile xtask for you; it assumes it already exists in target/debug/xtask. This should be true if you're using Hubris regularly, and saves significant amounts of time when opening a new file.

Adding a task

To create your own task, the easiest method is:

  • Copy task/template to a new name.
  • Edit its Cargo.toml with your name and a new package name.
  • Add it to the list of workspace members in the root Cargo.toml.
  • Add it to a system image by editing an app.toml file.
  • Run cargo xtask build to compile it.

A typical app.toml entry for a small task that uses no memory-mapped peripherals would read

[tasks.name_for_task_in_this_image]
name = "my-task-target-name"
priority = 1
requires = {flash = 1024, ram = 1024}
start = true

Graphing task relationships and priorities

A graph can be generated that show the relationships of the various tasks and their priorities. The resulting file is in Graphviz's dot format. Dot source can be included in Asciidoctor source or rendered to a variety of formats.

To create and view an SVG graph for gimletlet on Ubuntu, ensure that the graphviz package is installed. Then generate the graph:

$ cargo xtask graph -o gimletlet.dot app/gimletlet/app.toml
$ dot -Tsvg gimletlet.dot > gimletlet.svg
$ xdg-open gimletlet.svg

Generating all graphs under Linux

Bash commands to generate all graphs:

  APPS=( $(find app -name '*.toml' ! -name Cargo.toml) )
  for app in "${APPS[@]}"
  do
    out=$(basename ${app//\//_} .toml).dot
    svg=$(basename $out .dot).svg
    cargo xtask graph -o $out $app
    dot -Tsvg $out > $svg
  done
  first="${APPS[0]}"
  out="$(basename ${first//\//_} .toml).dot"
  svg="$(basename $out .dot).svg"
  xdg-open "${svg}"

If eog is the default viewer, opening the first SVG in a directory will allow cycling through all of the available graphs using the same window.

Using Hubris

Hubris is tightly coupled to its debugger, Humility, which is used for the commands below either implicitly (in cargo xtask flash) or explicitly (in cargo xtask humility).

If the humility binary is not available on your $PATH, the HUBRIS_HUMILITY_PATH environment variable may be used to provide the path to the binary.

Flash

An image within a Hubris archive can be flashed directly onto a target board by running cargo xtask flash and specifying the appropriate TOML file. This will run cargo xtask dist and then pass the resulting build archive to humility flash. humility will invoke either OpenOCD or pyOCD to flash the image; the exact invocation depends on the board and is encoded in the build archive.

  • LPCXpresso55S69: cargo xtask flash app/lpc55xpresso/app.toml
  • STM32F4 Discovery board: cargo xtask flash app/demo-stm32f4-discovery/app.toml
  • ST Nucleo-H743ZI2 board: cargo xtask flash app/demo-stm32h7-nucleo/app-h743.toml
  • ST Nucleo-H753ZI board: cargo xtask flash app/demo-stm32h7-nucleo/app-h753.toml
  • Gemini bringup board: cargo xtask flash app/gemini-bu/app.toml

Running Humility

Humility is run in situ by specifying an archive on a directly connected board, or postmortem by specifying a dump. As a convenience for development, Humility can also be run in situ by specifying the appropriate TOML, e.g. on a machine with an STM32F4 Discovery board directly attached:

$ cargo xtask humility app/demo-stm32f4-discovery/app.toml -- tasks
    Finished dev [optimized + debuginfo] target(s) in 0.17s
     Running `target/debug/xtask humility demo/app.toml -- tasks`
humility: attached via ST-Link
ID ADDR     TASK               GEN STATE    
 0 20000108 jefe                 0 Healthy(InRecv(None))     
 1 20000178 rcc_driver           0 Healthy(InRecv(None))     
 2 200001e8 usart_driver         0 Healthy(InRecv(None))     
 3 20000258 user_leds            0 Healthy(Runnable)          <-
 4 200002c8 ping                48 Healthy(Runnable)         
 5 20000338 pong                 0 Healthy(InRecv(None))     
 6 200003a8 idle                 0 Healthy(Runnable)         

Debugging with GDB

humility includes a gdb subcommand which attaches to a running system using arm-none-eabi-gdb, optionally running its own openocd instance based on configuration data in the build archive.

For convenience, there's also a cargo xtask gdb façade which calls humility with the appropriate build archive:

$ cargo xtask gdb app/demo-stm32f4-discovery/app.toml -- --run-openocd
# ... lots of output elided ...
task_idle::main () at task/idle/src/main.rs:14
14          loop {
Breakpoint 1 at 0x800434c: file /crates.io/cortex-m-rt-0.6.15/src/lib.rs, line 560.
Note: automatically using hardware breakpoints for read-only addresses.
semihosting is enabled

semihosting is enabled

(gdb)

Note that cargo xtask gdb will (by default) also run dist and flash, to ensure that the image on the chip is up to date. The -n/--noflash option skips these steps.

Testing Hubris

The Hubris kernel is tested with a dedicated test image that includes a test runner, assistant and test suite. The test image emits its results via ITM. While these results can be interpreted manually, humility test automates this. humility test itself is most easily run via cargo xtask test, which runs the equivalent of cargo xtask dist, cargo xtask flash and cargo xtask humility test. The exact invocation depends on the board:

  • LPCXpresso55S69: cargo xtask test test/tests-lpc55xpresso/app.toml
  • STM32F3 Discovery board: cargo xtask test test/tests-stm32fx/app-f3.toml
    Note: for this board, SB10 must be soldered closed for ITM to work
  • STM32F4 Discovery board: cargo xtask test test/tests-stm32fx/app.toml
  • ST Nucleo-H743ZI2 board: cargo xtask test test/tests-stm32h7/app-h743.toml
  • ST Nucleo-H753ZI board: cargo xtask test test/tests-stm32h7/app-h753.toml

Note: cargo xtask humility test runs OpenOCD to connect to the device. You must exit any other instances of OpenOCD that you have connected to the device before running tests.

See the documentation for humility test for details on test results.

Debugging tests

Output from tests is captured by humility test; sys_log!() calls to tests can be added and then captured in a humility test dump. To capture a dump from tests that are otherwise passing, use cargo xtask humility directly and pass the -d flag, e.g.:

$ cargo xtask humility test/tests-stm32fx/app.toml -- test -d
...
humility: attached via ST-Link
humility: TPIU sync packet found at offset 1
humility: ITM synchronization packet found at offset 12
humility: expecting 22 cases
humility: running test_send ... ok
...
humility: running test_timer_notify ... ok
humility: running test_timer_notify_past ... ok
humility: tests completed: pass
humility: test output dumped to hubris.testout.2

if one needs to both run GDB and the test suite, use cargo xtask gdb with the test image's TOML and the appropriate GDB file, and then place breakpoints at the test of interest.

Special cases

Gemini bringup board

See the Gemini Bringup Getting Started docs (internal Oxide repo)

STM32F3 Discovery boards

For the STM32F3 Discovery board, SB10 must be soldered closed for ITM to work! This solder bridge defaults to being open, which leaves SWO disconnected. See the STM32F3 Discovery User Manual (UM1570) for schematic and details.

LPCXpresso55S69 board

To use the LPCXpresso55S69, you will need pyOCD, version 0.27.0 or later.

The LPCXpresso55S69 is somewhat of a mess because the built-on on-chip debugger, LPC-Link2, does not correctly support SWO/SWV

If you have the stock LPC-Link2, it will report itself this way via pyocd list:

$ pyocd list
  #   Probe                                           Unique ID
-----------------------------------------------------------------
  0   NXP Semiconductors LPC-LINK2 CMSIS-DAP V5.361   JSAQCQIQ

It's also possible that you have the Segger J-Link firmware -- firmware that will make its odious presence known by prompting for you to accept license terms whenever running pyocd list!

$ pyocd list
  #   Probe                                                       Unique ID
-----------------------------------------------------------------------------
  0   Segger J-Link LPCXpresso V2 compiled Apr  4 2019 16:54:03   726424936

In either of these cases you must -- as a one-time step -- install new firmware on the LPC-Link2. The new firmware is a build of the (open source) DAPLink, which we affectionally call RickLink after the engineer who managed to get it all built -- no small feat!

There are two files that you will need, both contained in the Hubris repository:

You will additionally need the LPCScrypt program from NXP.

Here are the steps to install RickLink:

  1. Install the DFU jumper. This can be found next to the SWD header on the left side of the board; it is labelled "DFU".

  2. Run scripts/boot_lpcscrypt from the installed LPCScrypt software:

$ /usr/local/lpcscrypt/scripts/boot_lpcscrypt 
Looking for DFU devices with VID 1fc9 PID 000c ...
dfu-util -d 1fc9:000c -c 1 -i 0 -t 2048 -R  -D /usr/local/lpcscrypt/scripts/../bin/LPCScrypt_228.bin.hdr
Booted LPCScrypt target (1fc9:000c) with /usr/local/lpcscrypt/scripts/../bin/LPCScrypt_228.bin.hdr
$
  1. Run lpcscrypt clockslow:
$ /usr/local/lpcscrypt/bin/lpcscrypt clockslow
$
  1. Run lpcscrypt program +w1 0x0 BankA to overwrite existing firmware
$ /usr/local/lpcscrypt/bin/lpcscrypt program +w1 0x0 BankA
................
Programmed 524288 bytes to 0x1a000000 in 2.610s (196.165KB/sec)
$
  1. Run lpcscrypt program +c <path-to-lpc4322_bl_crc.bin> BankA:
$ /usr/local/lpcscrypt/bin/lpcscrypt program +c ~/hubris/support/lpc4322_bl_crc.bin BankA
..
Programmed 57344 bytes to 0x1a000000 in 0.827s (67.717KB/sec)
$
  1. Assuming it is successful, remove the DFU jumper and disconnect/reconnect USB

  2. There should now be a USB mass storage device named MAINTENANCE

# fdisk -l
Disk /dev/nvme0n1: 477 GiB, 512110190592 bytes, 1000215216 sectors
Disk model: Micron 2200S NVMe 512GB
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: A8653F99-39AB-4F67-A9C9-524A2864856E

Device             Start        End   Sectors   Size Type
/dev/nvme0n1p1      2048    1050623   1048576   512M EFI System
/dev/nvme0n1p2   1050624  967393279 966342656 460.8G Linux filesystem
/dev/nvme0n1p3 967393280 1000214527  32821248  15.7G Linux swap


Disk /dev/sda: 64.1 MiB, 67174400 bytes, 131200 sectors
Disk model: VFS
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000
# mount /dev/sda /mnt
# ls /mnt
DETAILS.TXT  PRODINFO.HTM
# cat /mnt/DETAILS.TXT
# DAPLink Firmware - see https://mbed.com/daplink
Unique ID: 02360b000d96e4fc00000000000000000000000097969905
HIC ID: 97969905
Auto Reset: 1
Automation allowed: 1
Overflow detection: 1
Daplink Mode: Interface
Interface Version: 0254
Bootloader Version: 0254
Git SHA: f499eb6ec4a847a2b78831fe1acc856fd8eb2f28
Local Mods: 1
USB Interfaces: MSD, CDC, HID, WebUSB
Bootloader CRC: 0x09974fb3
Interface CRC: 0x7174ab4c
Remount count: 0
URL: https://os.mbed.com/platforms/LPCXpresso55S69/
  1. Copy lpc4322_lpc55s69xpresso_if_rla_swo_hacks.bin to the USB drive
$ sudo cp ~/hubris/support/lpc4322_lpc55s69xpresso_if_rla_swo_hacks.bin /mnt
$
  1. Unmount (or otherwise sync) the USB drive:
# umount /mnt
#
  1. Unplug and replug the USB cable.

Verify that you are on the new firmware by running pyocd list:

$ pyocd list
  #   Probe                        Unique ID                                         
-------------------------------------------------------------------------------------
  0   LPCXpresso55S69 [lpc55s69]   02360b000d96e4fc00000000000000000000000097969905  

LPC55S28 on Gemini carrier board

Note that the RickLink running on the LPCXpresso55S69 can also be used as the debugger for the LPC55S28 on the Gemini carrier board. To do this, first, follow all of the instructions above to get RickLink onto your LPCXpresso55S69. Then:

  1. Using a soldering iron, solder a two-pin header on J5. J5 can be be found to the left of P1 and below the "Debugger" jumper (J3).

  2. Put a jumper on the new header

  3. Move the "Debugger" jumper (J3) to "Ext".

  4. Use a SWD cable (10-pin 2x5 1.27mm pitch cable) to connect the SWD on the LPCXpresso55S69 to the SWD underneath the carrier board on Gemini (J202)

(To allow your RickLink to once again debug its local LPC55S69, remove the jumper on J5 and move J3 to "Loc".)

Multiple boards simultaneously

If multiple probes are attached, tools may struggle to find the right one at the right time. In particular, OpenOCD will pick the first one that it finds; to force OpenOCD to pick a particular probe, you can ascertain the serial number of the probe (e.g., from humility probe) and then specify that serial number in the corresponding openocd.cfg by adding, e.g.:

interface hla
hla_serial 271828182845904523536028

(Where 271828182845904523536028 is the serial number of the probe.)

Updating ST-Link Firmware if necessary

It is common that debugging dongles, and development boards with embedded debug hardware like the Nucleo series, are delivered with older firmware.

You will not be able to use Humilty with outdated ST-Link firmware. Humility will tell you this is the case, for example when attempting to use humility test:

...
Warn : Adding extra erase range, 0x08020060 .. 0x0803ffff
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
humility: test failed: The firmware on the probe is outdated
Error: test failed

Follow this "ST-LINK firmware upgrade" link to find software and instructions necessary to install current firmware.

hubris's People

Contributors

aaron-hartwig avatar andrewjstone avatar arjenroodselaar avatar bcantrill avatar bnaecker avatar cbiffle avatar citrus-it avatar cvvletter avatar dancrossnyc avatar flihp avatar hawkw avatar jclulow avatar jeffallen avatar jgallagher avatar jmingov avatar jperkin avatar kevinji avatar kezhenxu94 avatar labbott avatar luqmana avatar lzrd avatar mchodzikiewicz avatar mkeeter avatar mx-shift avatar nathanaelhuffman avatar rmustacc avatar smklein avatar steveklabnik avatar wcampbell0x2a avatar wesolows avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hubris's Issues

Should we allow the kernel scheduler to idle?

Currently, the kernel panics if it runs out of schedulable tasks, meaning we have to remember to include an idle task in every image. This burns text and RAM -- perhaps we should let the scheduler idle internally?

Check how stm32h7 spi server handles client death.

The stm32h7 spi server supports a Lock operation that causes it to listen to only one client. If that client crashes before releasing the lock, the server gets woken with a dead code.

It is not immediately clear to me that the server does something reasonable when that happens -- should verify.

Task interaction integrity checks

We have discussed but not implemented several kinds of integrity checks on task interactions. I'm filing this issue to keep track of them and record progress.

The cases I'm aware of are as follows.

Uphill send

Check that SEND is always used from low to high priority. This is one of our mechanisms for avoiding deadlock. Validating this would be relatively cheap (a compare on the send path).

However, there is at least one piece of software that's currently violating the uphill send rule deliberately, and that's the test suite -- specifically, the way the suite and assistant task send back and forth. We could refactor it to fix this, potentially.

However, note that this winds up being a subset of the next thing:

SEND MAC

Impose Mandatory Access Control (MAC) on the SEND operation, limiting the set of tasks that a given task can SEND to. The set would be defined by the application depending on its needs. Tasks outside the set would appear not to exist to the sender, for all intents and purposes -- that is, attempting to access one would be a fault, though it might not specifically be a TaskIndexOutOfRange fault.

Having this can potentially subsume uphill send validation, by ensuring at build time that each task can only send to higher-priority tasks.

Closed RECV reverse MAC validation

A task should only be able to enter closed RECV to listen to a task that could potentially send something. This means we should probably check the SEND MAC information on closed RECV.

REPLY reverse MAC validation

If task A replies to task B, but static MAC rules indicate that B could never have sent a message to A, A is probably malfunctioning. We can detect this case if we have MAC rules. I am inclined to try to detect it, in the interest of catching every programming error as early as possible, but because we must trust servers more than clients in general, I feel like it's slightly less important than (say) validating on SEND.

Post filters

The POST operation does not currently exist, but I'm adding it in #224. This will let any task poke any other task's notifications, simulating interrupts and just generally being a nuisance. (This is why the guidance on interrupt handling is to check the source of truth for the interrupt events instead of simply trusting the notification bits.)

I could imagine a couple of levels of filters here.

  1. We could restrict which tasks can access the POST operation in the first place. POST is usually used by specialized servers that simulate interrupts -- the set I'm aware of is the supervisor, the network stack, and the STM32 external interrupt multiplexing driver that @labbott is writing.
  2. We could restrict which bits can be posted, either separately or in addition to (1). For instance, we could use the interrupt routing information to generate a "not a hardware interrupt" mask for each task's notification bits, and only allow those to be posted. Or in the extreme we could have ACLs per bit, but that feels too heavy to me.

I currently feel like (1) is the sweet spot there.

Hypovisor on lpc55: kernel stack may grow into hypovisor RAM

Per #209 I'm working on having the kernel stack grow away from kernel data, which on stm32 has it growing toward an unmapped section of address space that will bus-fault. This is likely to remain true on stm32, as stm32 does not need a hypovisor, only a bootloader -- so the bootloader should have no RAM needs after kernel boot.

On the lpc55, the hypovisor appears to reserve 16kiB of RAM permanently, and puts it at the base of the SRAM. This means the kernel stack will instead grow toward hypovisor space. If the kernel stack were to enter hypovisor space, we need a reliable trap in order to detect overflow.

Hypovisor runs in Secure, so the easiest way to achieve this may be to mark that section of SRAM as Secure-Only if we haven't already. This bug is open to remind us to check this.

most tasks will enter panic!() loops when dependencies restart

As of #76. when a task doesn't match the intended generation, the new generation will be returned in the failure message. This is fine in principle, but we currently have zero tasks that actually deal with this properly: they will see an error code that they don't recognize, and they will (generally) panic!(). Most problematically. on restart, they will do this again: they will still use Generation::default, which again will not match the current generation, they will again make a call that fails with an error that they don't recognize, and they will (again) fail and (again) panic. On the one hand, we could modify every task to deal with this error properly, but this can be pretty thorny -- and for most tasks, the right answer is to reset anyway. It seems that we (still) need an easy way for tasks to query as to the current generation of a task they wish to communicate with. The change that would involve the least delta would be to modify TaskId::for_index_and_gen such that Generation::default denotes that the kernel should be queried for the current task generation. Regardless of the approach we talk, we definitely need to make some change here: as it stands, we are a series of infinite loops, waiting to happen.

initialized RW data can evade size checks, inducing flash failure or worse

Add the following to (say) the venerable pong task:

diff --git a/task-pong/src/main.rs b/task-pong/src/main.rs
index 9d06ed8..b938d08 100644
--- a/task-pong/src/main.rs
+++ b/task-pong/src/main.rs
@@ -3,11 +3,16 @@
 
 use userlib::*;
 
+#[no_mangle]
+pub static mut KABOOM: [u32; 1024] = [0xdefec8d; 1024];
+
 #[export_name = "main"]
 pub fn main() -> ! {
     const TIMER_NOTIFICATION: u32 = 1;
     const INTERVAL: u64 = 500;
 
+    unsafe { KABOOM[0] = 1; }
+
     let mut response: u32 = 0;
 
     let user_leds = get_user_leds();

And adjust app.toml to add (only) the necessary DRAM:

diff --git a/gemini-bu/app.toml b/gemini-bu/app.toml
index ad3b570..c46de87 100644
--- a/gemini-bu/app.toml
+++ b/gemini-bu/app.toml
@@ -91,7 +91,7 @@ start = true
 path = "../task-pong"
 name = "task-pong"
 priority = 3
-requires = {flash = 8192, ram = 1024}
+requires = {flash = 8192, ram = 8192}
 start = true
 
 [tasks.idle]

The resulting app.toml will now fail to flash:

$ cargo xtask flash ./gemini-bu/app.toml 
...
** Programming Started **
Info : Device: STM32H74x/75x
Info : flash size probed value 2048
Info : STM32H7 flash has dual banks
Info : Bank (0) size is 1024 kb, base address is 0x8000000
Info : Padding image section 0 at 0x08004c74 with 13196 bytes
Info : Padding image section 1 at 0x0800a65c with 6564 bytes
Info : Padding image section 2 at 0x0800d488 with 2936 bytes
Info : Padding image section 3 at 0x0800f628 with 2520 bytes
Info : Padding image section 4 at 0x0801190c with 9972 bytes
Info : Padding image section 5 at 0x08015c7c with 9092 bytes
Info : Padding image section 6 at 0x08019534 with 2764 bytes
Error: Section at 0x0801c000 overlaps section ending at 0x0801c33c
Error: Flash write aborted.

If one should have the misfortune of having this task be the last in the app.toml, it will successfully flash -- but pong will be stuck in a fault loop:

$ cargo xtask humility ./gemini-bu/app.toml tasks
...
humility: attached via ST-Link
ID ADDR     TASK               GEN STATE    
 0 20000128 jefe                 0 Healthy(Runnable)          <-
 1 20000198 rcc_driver           0 Healthy(InRecv(None))     
 2 20000208 gpio_driver          0 Healthy(InRecv(None))     
 3 20000278 usart_driver         0 Healthy(InRecv(None))     
 4 200002e8 i2c_driver           0 Healthy(InRecv(None))     
 5 20000358 user_leds            0 Healthy(InRecv(None))     
 6 200003c8 idle                 0 Healthy(Runnable)         
 7 20000438 pong                30 Faulted { fault: MemoryAccess { address: Some(0x801e000), source: User }, original_state: Runnable } 

The fundamental problem is that we're creating initialized RW data -- it is in neither .rodata nor .bss. This data must be allocated twice: it must exist in flash (where it is initialized), but must be copied into RAM. But because the allocation is only checked against RAM, failure to allocate sufficient flash results in either a corrupt image (the first case), or an image that walks off the MPU-protected region when initializing data in userlib::_start (the second case). There are presumably many ways to fix this, but a straightforward one is to have the build process check that a task's allocated flash does not exceed its requested flash. Diff for this fix:

diff --git a/xtask/src/dist.rs b/xtask/src/dist.rs
index ed1c18c..7ed2542 100644
--- a/xtask/src/dist.rs
+++ b/xtask/src/dist.rs
@@ -141,7 +141,18 @@ pub fn package(verbose: bool, cfg: &Path) -> Result<()> {
             &task_names,
             &toml.secure,
         )?;
-        let ep = load_elf(&out.join(name), &mut all_output_sections)?;
+
+        let (ep, flash) = load_elf(&out.join(name), &mut all_output_sections)?;
+
+        if flash > task_toml.requires["flash"] as usize {
+            bail!(
+                "{} has insufficient flash: specified {} bytes, needs {}",
+                task_toml.name,
+                task_toml.requires["flash"],
+                flash
+            );
+        }
+
         entry_points.insert(name.clone(), ep);
     }
 
@@ -177,7 +188,7 @@ pub fn package(verbose: bool, cfg: &Path) -> Result<()> {
         "",
         &toml.secure,
     )?;
-    let kentry = load_elf(&out.join("kernel"), &mut all_output_sections)?;
+    let (kentry, _) = load_elf(&out.join("kernel"), &mut all_output_sections)?;
 
     // Write a map file, because that seems nice.
     let mut mapfile = File::create(&out.join("map.txt"))?;
@@ -779,7 +790,7 @@ fn load_srec(
 fn load_elf(
     input: &Path,
     output: &mut BTreeMap<u32, LoadSegment>,
-) -> Result<u32> {
+) -> Result<(u32, usize)> {
     use goblin::container::Container;
     use goblin::elf::program_header::PT_LOAD;
 
@@ -793,6 +804,8 @@ fn load_elf(
         bail!("this is not an ARM file");
     }
 
+    let mut flash = 0;
+
     // Good enough.
     for phdr in &elf.program_headers {
         // Skip sections that aren't intended to be loaded.
@@ -806,6 +819,8 @@ fn load_elf(
         // is loaded in flash but expected to be copied to RAM.
         let addr = phdr.p_paddr as u32;
 
+        flash += size;
+
         // Check for address overlap
         let range = addr..addr + size as u32;
         if let Some(overlap) = output.range(range.clone()).next() {
@@ -824,7 +839,11 @@ fn load_elf(
             },
         );
     }
-    Ok(elf.header.e_entry as u32)
+
+    // Return both our entry and the total allocated flash, allowing the
+    // caller to assure that the allocated flash does not exceed the task's
+    // required flash
+    Ok((elf.header.e_entry as u32, flash))
 }
 
 /// Keeps track of a build archive being constructed.

Running with this fix yields:

$ cargo xtask flash ./gemini-bu/app.toml 
...
Error: task-pong has insufficient flash: specified 8192 bytes, needs 9020

Blocking in closed kernel receive with a zero notification mask: should it be a fault?

We currently used closed receive against the kernel's virtual TaskId in cases where we only wish to receive notifications, not messages. It's currently possible to call this with a notification mask with all bits clear. In this case, the task will never receive notifications. This is okay-ish in our intended applications because an application-level watchdog will eventually restart the dead task, but, it is not ideal.

We could detect this immediately on entry to the kernel and turn it into a UsageError fault against the task, since it most likely represents a programming error.

I think we've got a TODO about this somewhere in the code but I don't see an issue filed, so, here's an issue. Thanks to an anonymous friend of @steveklabnik's for reminding me about this.

`xtask flash` should get its instructions from app.toml somehow

xtask flash currently has a hardcoded table of board names for finding flashing config files. That's going to become annoying. It should take direction from app.toml instead.

I'm imagining something like

[flashing]
method = "openocd"
config = "../whatever.cfg"

where the paths are relative to the app.toml itself, as usual.

ringbuf in its current form is likely unsound

I'm writing this down just so that we don't forget. :-)

The way the ringbuf macro manages its static mut storage is likely unsound and we should take a pass over it.

It's "unsound" in the "could easily break in weird ways later" sense, not in the "currently doesn't work" sense -- it works fine.

Let's keep more generation bits in the kernel

So, aspects of the IPC ABI mean that we need to keep the generation sent by callers relatively small (6 bits, currently), because we're real short on registers on ARMv7-M. (Thank goodness we're not trying to support x86.)

This is probably fine for the original purpose of generations, which is to provide best-effort assurance that the task you're talking to has not restarted since you last spoke to it.

However, generations are also finding use as a "crash counter," in which case having them wrap around can be deceptive. We had a case where we observed a crashing task having GEN 0, which sure looks like it hasn't ever crashed.

We can fix this pretty easily by expanding the generation field in the task record to something like a u32, and only comparing the bottom N bits for checking IPC generation.

Kernel access to unprivileged task memory may violate Rust aliasing rules

Current status summary

Most of this is now done, we just need to detect the DMA attribute in Task::can_access.

Original description follows

In the kernel, we use the USlice type to model unprivileged memory regions. To access the contents of those regions from the kernel, we normally use can_read to verify access for a specific task, followed by assume_readable to turn the abstract memory region into a kernel-native &[T].

The reason why we can ever do this is that the execution of the kernel is mutually exclusive with all tasks, by design. While the kernel is running, all tasks are frozen. (Importantly, this is why all of our discussions around multi-core have assumed a kernel per core.) Because the USlice and Task implementations ensure the basic properties like correct alignment and access permissions, we can get a valid Rust slice that we can access without racing unprivileged task code.

However -- while this means it's correct sometimes, it is not sufficient to ensure that it's always correct. Because we use a Rust shared reference to access the task memory, we are expressing to the compiler that the contents of that memory can't change, and that assumption does not hold in two cases:

  1. The memory is RAM but the task has arranged for DMA to be writing into it.
  2. The memory is not RAM and is in fact memory-mapped peripheral registers.

As a concrete example, consider the panic path, which was noted by @iljavs2 in our IOActive review. It checks that the alleged message is valid ASCII and then does from_utf8_unchecked (to avoid the enormous text cost of the checked version of from_utf8). If the memory is changing, we have a TOCTOU issue that could produce a non-UTF8 str in the kernel. (This specific issue can be eliminated by not klog-ing the panic message, since the supervisor is responsible for that anyway, but the point remains.)

One possible fix

In order to fix this, we likely need to have the kernel distinguish between RAM and registers, and DMA-capable RAM vs not. We already have the latter attribute, but we do not currently record the RAM-register distinction in the image, though we do have it at build time.

The kernel could then decline to access either class of region.

This fix might be heavy-handed: it would prevent a task from putting a lease table or panic message anywhere in DMA-capable RAM. Nothing prevents a task from marking all of its RAM as DMA-capable, today -- it produces a performance hit on Cortex-M7 by bypassing cache, but is otherwise fine.

The fix is also technically insufficient, as a misbehaving task could set up DMA to non-DMA-marked RAM.

Another possible fix

We may want to eliminate the assume_readable / assume_writable operations that produce Rust references, and always access unprivileged memory using explicit pointer accesses. My use of kernel-native slices for accessing unprivileged memory was a convenience that simply be incorrect. More analysis needed.

"cargo clean" does not force relink of kernel

Build, say, demo, and observe resulting app table:

$ cargo xtask dist demo/app.toml
...
$ humility apptable ./target/demo/dist/kernel | more
App = {
        magic: 0x1defa7a1,
        task_count: 0x7,
        region_count: 0x13,
        irq_count: 0x1,
        fault_notification: 0x1,
        zeroed_expansion_space: [
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0,
            0x0
        ]
    }
...

Now change the app.toml to (say) send notifications to task 2:

diff --git a/demo/app.toml b/demo/app.toml
index f5af15f..de0d5b7 100644
--- a/demo/app.toml
+++ b/demo/app.toml
@@ -19,7 +19,7 @@ requires = {flash = 65536, ram = 4096}
 features = ["itm"]
 
 [supervisor]
-notification = 1
+notification = 2
 
 [outputs.flash]
 address = 0x08000000

Thanks to @steveklabnik's work in #87, it correctly realizes that each package should be cargo clean'd:

$ cargo xtask dist demo/app.toml
    Finished dev [optimized + debuginfo] target(s) in 0.21s
     Running `target/debug/xtask dist demo/app.toml`
flash = 8000000..8040000
ram = 20000000..2001c000
kernel: {"flash": 8000000..8010000, "ram": 20000000..20001000}
app.toml has changed; rebuilding all tasks
cleaning demo
cleaning task-jefe
cleaning drv-stm32f4-rcc
cleaning drv-stm32f4-usart
cleaning drv-user-leds
cleaning task-ping
cleaning task-pong
cleaning task-idle
building path demo/../task-jefe
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-jefe -> target/demo/dist/jefe
building path demo/../drv/stm32f4-rcc
    Finished release [optimized + debuginfo] target(s) in 0.15s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/drv-stm32f4-rcc -> target/demo/dist/rcc_driver
building path demo/../drv/stm32f4-usart
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/drv-stm32f4-usart -> target/demo/dist/usart_driver
building path demo/../drv/user-leds
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/drv-user-leds -> target/demo/dist/user_leds
building path demo/../task-ping
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-ping -> target/demo/dist/ping
building path demo/../task-pong
    Finished release [optimized + debuginfo] target(s) in 0.13s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-pong -> target/demo/dist/pong
building path demo/../task-idle
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-idle -> target/demo/dist/idle
building path demo/../demo
    Finished release [optimized + debuginfo] target(s) in 0.15s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/demo -> target/demo/dist/kernel

But note that the kernel has not in fact been rebuilt, and contained the old app table:

$ humility apptable ./target/demo/dist/kernel | grep fault_notification
        fault_notification: 0x1,

If, say, a kernel source file is touched, the kernel will be rebuilt:

$ touch ./kern/src/arch/arm_m.rs
$ cargo xtask dist demo/app.toml
    Finished dev [optimized + debuginfo] target(s) in 0.19s
     Running `target/debug/xtask dist demo/app.toml`
flash = 8000000..8040000
ram = 20000000..2001c000
kernel: {"flash": 8000000..8010000, "ram": 20000000..20001000}
building path demo/../task-jefe
    Finished release [optimized + debuginfo] target(s) in 0.13s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-jefe -> target/demo/dist/jefe
building path demo/../drv/stm32f4-rcc
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/drv-stm32f4-rcc -> target/demo/dist/rcc_driver
building path demo/../drv/stm32f4-usart
    Finished release [optimized + debuginfo] target(s) in 0.13s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/drv-stm32f4-usart -> target/demo/dist/usart_driver
building path demo/../drv/user-leds
    Finished release [optimized + debuginfo] target(s) in 0.14s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/drv-user-leds -> target/demo/dist/user_leds
building path demo/../task-ping
    Finished release [optimized + debuginfo] target(s) in 0.13s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-ping -> target/demo/dist/ping
building path demo/../task-pong
    Finished release [optimized + debuginfo] target(s) in 0.13s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-pong -> target/demo/dist/pong
building path demo/../task-idle
    Finished release [optimized + debuginfo] target(s) in 0.13s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/task-idle -> target/demo/dist/idle
building path demo/../demo
   Compiling kern v0.1.0 (/home/bmc/hubris/kern)
   Compiling demo v0.1.0 (/home/bmc/hubris/demo)
    Finished release [optimized + debuginfo] target(s) in 1.67s
/home/bmc/hubris/target/thumbv7em-none-eabihf/release/demo -> target/demo/dist/kernel

And the app table is now what we would expect:

$ humility apptable ./target/demo/dist/kernel | grep fault_notification
        fault_notification: 0x2,

(It should go without saying that one should not attempt to run such a kernel!)

The problem appears to be that cargo clean is not in fact blowing away the built kernel artifact:

$ find target -name demo-\*
target/thumbv7em-none-eabihf/release/.fingerprint/demo-e7eb269c08982ab0
target/thumbv7em-none-eabihf/release/deps/demo-e7eb269c08982ab0
target/thumbv7em-none-eabihf/release/deps/demo-e7eb269c08982ab0.d
$ cargo clean -p demo
$ find target -name demo-\*
target/thumbv7em-none-eabihf/release/.fingerprint/demo-e7eb269c08982ab0
target/thumbv7em-none-eabihf/release/deps/demo-e7eb269c08982ab0
target/thumbv7em-none-eabihf/release/deps/demo-e7eb269c08982ab0.d

If the build artifact is manually blown away, the kernel is properly rebuilt -- but I couldn't seem to find the incantation for cargo clean to do this...

Wouldn't it be nice to refer to interrupts in the TOML by name

Currently we're doing stuff like

[tasks.spi_driver]
# irrelevant bits omitted
interrupts = {51 = 1}

Good old interrupt 51. ...

Better would be

interrupts = {spi3 = 1}

The SVD files consumed by the PAC crates during their build process contain enough information to do this, but the PAC crates don't produce any kind of reflective interface we could use to reason about interrupts on x86. Too bad, that. We may have to build our own thing.

HIFFY_DATA missing on non-Gemini H7 boards

Due to some recent changes, HIFFY_DATA is being optimized away, leading to the following failure mode in Humility:

humility: hiffy failed: expected hiffy interface not found: variable HIFFY_DATA not found

We need a better long-term fix for this, but right now the fix ("fix") is to trick the compiler into thinking HIFFY_DATA is being used.

cargo expand support for tasks

Because we don't use cargo build to build a task, cargo-expand doesn't work for them. It would be nice to gain support for this somehow, possibly by integrating it into xtask somehow?

peripheral sizes should be enforced to be powers of two

Currently, peripheral sizes must be powers of two -- and if they aren't, they are (silently) undersized; from apply_memory_protection:

        // This is a bit of a hack; it works if the size is a power of two, but
        // will undersize the region if it isn't. We really need to validate the
        // regions at boot time with architecture-specific logic....
        let l2size = 30 - region.size.leading_zeros();
        let rasr = (xn as u32) << 28
            | ap << 24
            | tex << 19
            | scb << 16
            | l2size << 1
            | (1 << 0); // enable
        unsafe {
            mpu.rbar.write(rbar);
            mpu.rasr.write(rasr);
        }

This behavior is currently silent (and was found due to the GPIO sizing on STM32H7 of 0x2c00 and the manipulation of a port that maps to the upper 0xc00 bytes of that range inducing a fault); it would be great to enforce this at several levels -- presumably starting with the build, which should reject sizes that aren't powers of two.

USlice begin and end slice validation in covers seems confusing.

The code inside covers compares the self_end with the slice base address and the slice end with self.base. This may or may not be correct, however, it's not self explanatory (as you'd expect a self_end to be compared with a slice end and self.base to be compared with the slice base). Either a comment explaining this would make sense, or, in case it is wrong, a fix for this issue.

hubris-master\kern\src\app.rs

impl RegionDescExt for abi::RegionDesc {
    /// Tests whether `slice` is fully enclosed by this region.
    fn covers<T>(&self, slice: &crate::umem::USlice<T>) -> bool {
        let self_end =
            self.base.wrapping_add(self.size).wrapping_sub(1) as usize;
        if let Some(slice_end) = slice.last_byte_addr() {
            // Slice is not empty
            self_end >= slice.base_addr() && slice_end >= self.base as usize   <-- should this not be reversed? self_end >= slice_end && slice.base_addr() >= self.base
        } else {
            self_end >= slice.base_addr()
                && slice.base_addr() >= self.base as usize
        }
    }
}

UB in userlib::hl::send: pointer passed to from_raw_parts_mut must point to initialized T

let mut response: MaybeUninit<M::Response> = MaybeUninit::uninit();
let rslice = unsafe {
core::slice::from_raw_parts_mut(
response.as_mut_ptr() as *mut u8,
core::mem::size_of_val(&response),
)
};

slice::from_raw_parts_mut documents that

data must point to len consecutive properly initialized values of type T.

I see that M::Response requires zerocopy::FromBytes. I think this trait bound means that the pointed-to data for the slice could be initialized with core::mem::zeroed(). But it's totally unclear to me if this initialization imposes too much overhead, and the interface for sys_send should be to pass through the MaybeUninit-ness of the data by using a &[MaybeUninit<u8>] instead of &[u8].

need mechanism to query the current generation of a task

Currently, if a task that one needs dies, subsequent calls will fail. While it is true that a savvy (or resigned) client could simply increment the generation number looking for the current generation of its server, there should really be a way of getting this. @cbiffle posited that a Dead response could return the current generation in its payload, which seems like it would solve the problem rather nicely.

Kernel-internal NULL dereference can likely succeed

I'm not sure we've done anything to trap if the kernel dereferences NULL. We should consider using the MPU to achieve this, perhaps by dedicating a region slot to an inaccessible region positioned around NULL, or perhaps by swapping one in when we enter the kernel.

On STM32 we can just revoke access to the bottom X bytes of address space (where X is large, about 128 MiB), because the vector table that initially lives there is aliased from its "real" address up higher.

On LPC55 I'm less confident. @labbott ?

The ARM ecosystem is complex enough that I'm sure there's some micro out there where the kernel needs access to zero, but let's try to avoid buying it. :-)

Lease operations currently provide a way to arbitrarily kill other tasks

Noticed this in passing: one of the side effects of the borrow_lease operation in the kernel is that, if applied to a task that is not waiting in reply, it assumes that task has defected (claimed to loan things and not actually loaning them) and kills it.

I believe that this currently means you can use any of the borrow syscalls to kill any task you like. That's bad. We should alter this logic.

We're probably gonna have to change the syscall ABI.

tl;dr: we are passing arguments to the kernel / returning values from the kernel in registers r6 and r7. There's a reason for this. However, LLVM wants to use those for the base and frame pointer, respectively, and there is no way to tell it not to. This is complicating our syscall stubs and we might want to revisit the ABI design.

stack overflows fail silently and confusingly

While working on #70, I have run into multiple stack overflows. These were entirely my fault, but it took me far too long to realize what was going on, diagnose and fix it. Some of that is my relative inexperience, of course, but... yeah. Basically, when this happens, you get a silent failure.

A screenshot of the first one, since the branch is likely to get deleted:

image

Second, my tasks were hitting a panic, and then blowing up, which was expected, but silently, which was not. This is because I had set the ram for my task too low, because I copy/pasted the config from task-idle, thinking that a smaller task is a good idea to start with.

It would be nice if it was much more obvious that this kind of failure was causing these problems, though I won't be making this mistake again (I hope...).

Consider validating 0-sized slices in can_access

can_access is used in the kernel to validate incoming USlices, to make sure they're sane. There is some special casing for 0-sized slices, essentially, they're not validated. There is a comment that explains this, it is to account for
literals like &[] which apparently produce a base address of 0 + sizeof::<T>(), however, it might make sense to special case only that and still validate other 0-sized USlices.

In general it sounds reasonable not to validate 0-sized slices, since the slice is 0 bytes long. However, functionally similar APIs like can_access on different platforms (e.g. ProbeForRead, ProbeForWrite on windows) have the same behavior and it has lead to numerous bugs being exploitable. While, yes, technically it's not can_access' fault that someone else does a non 0-sized read or write on a user provided USlice of 0-size, the point is that this behavior exacerbates the problem. It should also be noted that due to the nature of rust, it's significantly less likely that issues like these would come up. However, given the history of behavior like this, it would probably make sense to try and validate a USlice of 0 size, and simply special case &[].

hubris-master\kern\src\task.rs

    pub fn can_access<T>(
        &self,
        slice: &USlice<T>,
        atts: RegionAttributes,
    ) -> bool {
        if slice.is_empty() { // <-- this might not be a good design decision. not checking for 0-sized buffers will make certains bugs exploitable. 
            // We deliberately omit tests for empty slices, as they confer no
            // authority as far as the kernel is concerned. This is pretty
            // important because a literal like `&[]` tends to produce a base
            // address of `0 + sizeof::<T>()`, which is almost certainly invalid
            // according to the task's region map... but fine with us.
            return true;
        }
        self.region_table.iter().any(|region| {
            region.covers(slice)
                && region.attributes.contains(atts)
                && !region.attributes.contains(RegionAttributes::DEVICE)
        })
    } ```

Kernel out of startup-heap behavior is currently difficult to debug

The kernel currently uses a bump-pointer allocator to allocate task structures at startup, from its free RAM section. When the free RAM section is not long enough for this, it will panic, and this panic is difficult to diagnose if you're not watching over ITM at the time. We should fix this. Potential approaches off the top of my head include

  • Figure out how to allocate that stuff statically, converting this into a build failure.
  • Make the panic behavior better
  • Add kernel panic diagnosis to humilty

a beginner-friendly “getting started” tutorial

I'm a hobbyist tinkerer who likes to play with microcontrollers and small computers like arduinos, raspis and feathers. I began reading the documentation for hubris hoping to get a taste for what running a rust-based microkernel on something like the new rp2040 feather would feel like and I'll admit that I'm overwhelmed by all the low level systems details and lack of hand-holding! I'm sure the choice to eschew virtual addressing - for instance - was an important one architecturally, but I'm at the stage where I want to flash my feather and blink an LED and I'm not clear on where to go for that.

I think what I would like to see is a concrete tutorial based on something you could buy from adafruit or sparkfun and something simple like blinking an LED, detecting a button press or buzzing a peizo. I would be tempted to copy something else structurally, or at least drawing inspo from the arduino and circuitpython projects.

I get if this is a low priority - oxide's goals don't seem to hinge on the hobbyist market - and I certainly don't want to be demanding, but I think it would make it a lot more approachable for me specifically ;) and hopefully I'm not the only one.

Thanks! Hope this is helpful!

kipc::read_task_status()/kipc::restart_task() panic kernel iff target equals NUM_TASKS

In writing the fault injection operation, I observed that both kipc::read_task_status and kipc::restart_task have an off-by-one error in that they only check if the target exceeds the task table length. If one specifies a target that equals NUM_TASKS, the kernel will panic, e.g.:

panicked at 'index out of bounds: the len is 4 but the index is 4', kern/src/kipc.rs:94:9

This will be fixed (and tests will be added) as part of the jefe external control and fault injection work.

We should firm up the semantics of `userlib::sleep*`

So, we've kind of avoided dealing with the timebase problem. (Well, okay, mostly me.)

Currently we express time in all APIs as "system ticks," where the system tick is set for the application (board+image) to whatever you want .... but in practice is milliseconds. And basically all code assumes it's milliseconds.

There are two problems with this.

  1. We often need timing more precise than milliseconds, but changing the tick frequency, while easy, would break basically all drivers.
  2. The userlib time-related APIs don't actually do what you'd expect.

On that second point -- if you call sleep_for(1), you will (in the absence of higher priority tasks keeping you from being scheduled at all) sleep for somewhere between 0ms and 1ms. In other words, it's really sleep_for_at_most_ms. Often, we actually want at least.

The actual kernel time API is based on absolute deadlines rather than intervals (basically to keep the kernel out of this argument) so I think we could fix this part in userlib.

Cleaning everything on app.toml changes shouldn't really be necessary

Currently, xtask dist runs around cleaning things whenever app.toml changes. This was originally done so that switching between apps didn't accidentally link in each others' objects. It's kind of annoying, however, and shouldn't be strictly necessary ... if we've got all the various build dependencies right. It may be masking cases where we forgot to (say) emit a cargo:rebuild-if-whatever line from the build script.

It would be great for one of us to investigate and make this more precise, as it should dramatically reduce build times for people working on more than one app.

It's also going to become increasingly important as we move more config information into the toml, since right now, changing (say) the proposed i2c config tree will force a rebuild of all tasks, which is overkill.

Fix gdb config generation on Windows:

Currently,

\Users\steve\scoop\apps\gcc-arm-none-eabi\current\bin\arm-none-eabi-gdb.exe: warning: Couldn't determine a path for the index cache directory. 
                                                                                                                      
 Reading symbols from target/dist/combined.elf...                                                                                    
(No debugging symbols found in target/dist/combined.elf)                                                                            
add symbol table from file "target/distkernel" 
                                                                                     
target/dist/script.gdb:1: Error in sourced command file:                                                                            
target/distkernel: No such file or directory.   

this is because script.gdb looks like this:

add-symbol-file target/dist\kernel
add-symbol-file target/dist\jefe
add-symbol-file target/dist\rcc_driver
add-symbol-file target/dist\usart_driver
add-symbol-file target/dist\ping
add-symbol-file target/dist\pong
add-symbol-file target/dist\idle

and gdb does not like the \. Changing them to / works just fine, but we shouldn't require manual intervention to get things going, of course

Implement `post` syscall

We do not currently have a way to poke notification bits from taskyland, because we haven't needed it. The original design proposal included a post operation for doing this, which would let a task -- subject to MAC eventually -- OR a word into another task's notification bits, possibly waking it.

We're approaching some use cases where we'll need to have this implemented.

Allow kernel to be built without app table

Kernel depends on the contents of the app table at runtime but only the location of the table at build time. Since the app table includes the entry point of each task, it necessarily depends on having built all of the tasks. If we use tricks similar to how peer tasks are resolved, the compilation of the kernel will no longer be dependent on the build output of the tasks.

SPI over HIF should have parameterized offset

Right now, we can't do multiple SPI transactions in a single HIF program, because spi_read/write always use data[0..].

We should parameterize the offset within the data array to make this possible. This probably means adding another argument, which would be a breaking change for Humility as well.

Separate task stack requirement from other RAM, enforce separation

Currently, tasks request amounts of flash and RAM. The RAM is used for both their stack, and any statics (including, potentially, the RAM handed to an allocator). As on lots of early machines, the stack grows down toward the statics. As on those early machines, the reason I did this initially was to be squidgy with resource allocation: if you need 700 bytes of stack and 200 bytes of static, sneaking those into the same 1024-byte region saves memory compared to dedicating 1024 and 256 bytes, respectively, as required by the ARMv7-M MPU model.

This creates the potential for the stack to grow into the statics and begin randomly corrupting things. This can happen without an explicit stack overflow trap, because we only detect overflow when the stack reaches the bottom of RAM -- which is by definition too late.

The least intrusive way to fix this is to swap the two areas of RAM, so that the stack grows away from statics toward the bottom of the RAM region (and thus toward an unmapped and reliably trapping section of address space). This hack works because stack overflows happen in compiled structured programming languages, but stack underflows do not, in practice, as we are not writing Forth. To do this, we need to know at link time the maximum size of the stack for each task, which is information we don't currently have.

I suggest that we add this information to each task's entry in the app.toml. I've been concerned that it would be yet another fiddly parameter to tweak during development, but the operating system's entire userbase (namely @bcantrill @labbott @steveklabnik and @kc8apf) appear to agree that this is fine in exchange for finer-grained failure detection, proving once again that I chose to work with the correct people. ;-)

Having the stack size information enables another option on ARMv8-M: continue to have the two areas grow toward each other, but use the newfangled PSPLIM register to impose a hardware-checked growth limit on the stack. This would require changes to the linker script and context switch code. I think it's interesting, but we'd need to be able to articulate why it is better than having the stack grow toward unmapped address space. Perhaps it allows for more precise error reporting?

I'm assuming that I'll be doing this work, but just in case, I expect the task to involve the following:

  1. Add a stack size parameter to the task TOML record.
  2. Assign appropriate stack sizes to each task by trial and error. (Note: we can do this because we don't support debug builds, which have wildly variable stack usage.)
  3. Add an additional environment variable hack to forward this information through the packager xtask to the userlib build.rs, or funnel it through one of the existing ones.
  4. Output an additional linker script fragment to communicate the stack size to the linker.
  5. Alter the existing user linker script to swap stack and data/bss by reserving N bytes and then defining the _initial_stack symbol or whatever (currently set to ORIGIN(RAM) + LENGTH(RAM))

No changes should be required in context switching or fault handling, unless we decide that using PSPLIM would be nice.

Task restarts should not cause time travel

Task restarts are implemented as of yesterday, which got me thinking through the corner cases in restarts and IPC. The current implementation has quite a few corner cases that are poorly handled.

Current situation

Each task has a generation number, which distinguishes successive incarnations of a task (mod small number). This is used to alert peers interacting with a task if that task falls over during interactions: they start getting errors (DEAD) until they update their target generation number. The restart_task algorithm increments the generation number to facilitate this.

This is a good start, but is not sufficient to cause bewildering and error-prone interactions between tasks. Let's consider a few cases.

The reply that never comes. Task A sends to task B, which receives the message and then crashes. B is restarted. A is still blocked in InReply(B), but B has no idea there is a client waiting. A is now dead.

Reply after reincarnation. Same scenario, except -- for whatever reason -- B decides to reply to A. This should not work, but (on current master) does, because currently task state is recorded without peer generation numbers.

Misdirected receive. "Directed receive" is a receive operation from a specific peer only. It isn't exposed in the syscall interface, but is implemented in the kernel. Were we to expose it, task A could wait for messages from task B, which crashes. Task A's receive either (1) can never be fulfilled, or (2) can be fulfilled by any random message from a future incarnation of B. This is bad, because directed receives are expected to be used as part of back-and-forth messaging protocols, which is exactly where you want to detect a crashing peer.

The send race. Task A sends to B while B is doing something else -- A is blocked in InSend(B). B crashes and is restarted. When B goes to receive, it will receive the message from A (because, again, task states do not currently involve generation numbers), and A will get its message processed without receiving the DEAD response code that it would have received had the send not blocked. This is a system-load-dependent behavior that could produce nasty, difficult-to-reproduce bugs.

Faulty fault recovery. In a system where a supervisor attempts to recover some tasks from fault conditions -- like, hypothetically, a system that implemented virtual memory -- a task might sit in a fault condition for an arbitrary period of time before returning to Healthy status. Because IPC operations can trigger faults, the Healthy status that gets restored may very well be an IPC-related status naming a peer B. If the peer B has restarted while the task was waiting in fault, the resumed task will now be in one of the previously described conditions through no fault of its own. (While I do not intend to implement virtual memory / demand paging in any of our currently planned systems, there are some cases where fault recovery might make sense, such as emulating access to privileged memory-mapped peripherals, or debug trap handling.)

Fixing this

If A uses an IPC operation at B, and B gets restarted before feedback is returned to A, the feedback returned to A should indicate DEAD.

  • If A is blocked InSend(B) or InReply(B) when B is restarted, A should be unblocked with a DEAD response code. (Note that B's generation number is checked before blocking A, so the case where B restarts before the operation is issued has already been covered.)
  • If A is in directed receive (InRecv(Some(B))) when B is restarted, A should be unblocked with DEAD.
  • Task SchedState values should include generation number in addition to task index, so that we can notice a generation mismatch when resuming a task from a faulted state. (This also makes any bugs in the previous two bullets less likely to produce time-travel effects where a message is received from a "future" task version.)
  • Any kernel operations that move tasks between the blocked states must also consider this.

Stated in terms of a kernel invariant, we get:

  • Type level: Any Healthy SchedState of any task that names another (peer) task must use a generation number (i.e. use a TaskId combining index and generation).
  • Runtime invariant: Any Healthy SchedState that names another (peer) task must refer to the current generation of that task. If this is ever broken, it means we have failed to accurately report a DEAD condition to that task.

I'm going to work through the implementations of the various cases and (1) fix the bogus behavior and (2) see if I can find ways to make the behavior harder to produce under maintenance.

changes in app.toml aren't reflected in kernel build

Changes to the system definition in app.toml don't necessarily induce a rebuild/relink of the kernel, resulting in run-time inconsistencies (including but not restricted to panics).

To see this, first build a demo (i.e., STM32F407):

$ cargo xtask dist -v demo/app.toml

Look at the program headers:

$ readelf -l ./target/demo/dist/kernel 

Elf file type is EXEC (Executable file)
Entry point 0x80001a9
There are 4 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000120 0x08000000 0x08000000 0x048ac 0x048ac R E 0x20
  LOAD           0x0049ec 0x080048ac 0x080048ac 0x00204 0x00204 R E 0x20
  LOAD           0x004c00 0x20000000 0x20000000 0x00000 0x00020 RW  0x20
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0

 Section to Segment mapping:
  Segment Sections...
   00     .vector_table .text .rodata 
   01     .hubris_app_table 
   02     .bss 
   03     

Now change (e.g.) the starting address:

diff --git a/demo/app.toml b/demo/app.toml
index f5af15f..f76ae38 100644
--- a/demo/app.toml
+++ b/demo/app.toml
@@ -28,7 +28,7 @@ read = true
 execute = true

 [outputs.ram]
-address = 0x20000000
+address = 0x20001000
 size = 114688
 read = true
 write = true

With that change alone, rebuild:

$ cargo xtask dist -v demo/app.toml

And note that the program header hasn't changed (specifically, the VirtAddr for the RAM-based program header is still 0x20000000):

$ readelf -l ./target/demo/dist/kernel

Elf file type is EXEC (Executable file)
Entry point 0x80001a9
There are 4 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000120 0x08000000 0x08000000 0x048ac 0x048ac R E 0x20
  LOAD           0x0049ec 0x080048ac 0x080048ac 0x00204 0x00204 R E 0x20
  LOAD           0x004c00 0x20000000 0x20000000 0x00000 0x00020 RW  0x20
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0

 Section to Segment mapping:
  Segment Sections...
   00     .vector_table .text .rodata
   01     .hubris_app_table
   02     .bss
   03

Now touch one of the kernel dependencies and rebuild; the kernel is rebuilt and the program header changes as expected (i.e., VirtAddr for the RAM-based program header is now 0x20001000):

$ touch ./kern/src/arch/arm_m.rs
$ cargo xtask dist -v demo/app.toml
...
$ readelf -l ./target/demo/dist/kernel

Elf file type is EXEC (Executable file)
Entry point 0x80001a9
There are 4 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000120 0x08000000 0x08000000 0x048ac 0x048ac R E 0x20
  LOAD           0x0049ec 0x080048ac 0x080048ac 0x00204 0x00204 R E 0x20
  LOAD           0x004c00 0x20001000 0x20001000 0x00000 0x00020 RW  0x20
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0

 Section to Segment mapping:
  Segment Sections...
   00     .vector_table .text .rodata 
   01     .hubris_app_table 
   02     .bss 
   03     

We need to rebuild the kernel when anything that affects the app.toml changes -- and note that in the testing rework, the same kernel can have multiple app.tomls point to it; it may be easier to always relink the kernel?

Kernel syscall argument handling is too boilerplatey

The AsFooArgs structs in kern/src/task.rs were my first attempt at having an abstraction layer in syscall argument handling, to insulate most of the code from architecture and ABI changes. They work but adding a syscall requires kind of a lot of typing.

I bet we could make this simpler.

Kernel stack overflows aren't well caught right now

Stack overflows in the kernel aren't detected. They're also somewhat unlikely, since the kernel itself is non-preemptible, but, in theory, a stack overflow could corrupt data.

The most general way to fix this is to have the kernel stack grow toward a permanently inaccessible guard region. These are easiest to find at the edge of a memory, where they'll tend to cause a bus error, but that's not always practical. They can be created by the MPU, but they need to be inaccessible to privileged mode, meaning they can't overlap with some task or we won't be able to accept messages from that section of task RAM.

If we use an MPU guard region, it either needs to permanently occupy one of the limited MPU slots, or be swapped in really early on kernel entry and replaced on kernel exit. These both have drawbacks; the first one burns one of eight slots; while the second one increases the cost of syscall entry/exit for calls that don't cause a context switch, which is a lot of calls. (On such calls we are careful not to update the MPU.)

Integer overflow in last_byte_addr

last_byte_addr is called to grab the highest address from a USlice it is called by covers which is used by can_access to validate a USlice. This method contains an integer overflow if a large length is specified when multiplying with the size of the type. This can be problematic when trying to validate an invalid or malicious USlice, as the address would wrap and be shorter than what the USlice really describes.

hubris-master\kern\src\umem.rs

    pub fn last_byte_addr(&self) -> Option<usize> {
        // This implementation would be wrong for ZSTs, but we blocked them at
        // construction.
        let size_in_bytes = self.length * core::mem::size_of::<T>(); <-- this can int overflow if self.length is very large 
        if size_in_bytes == 0 {
            None
        } else {
            Some(
                self.base_address
                    .wrapping_add(size_in_bytes)
                    .wrapping_sub(1),
            )
        }
    }

This seems like a one-off as USlice::from_raw for example does this correctly:

        let size_in_bytes = length
            .checked_mul(core::mem::size_of::<T>())
            .ok_or(UsageError::InvalidSlice)?;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.