random bloggings

/dev/blograndom

Who needs social media when you can dd if=/dev/brainfart of=/dev/blograndom !?

2026-09-22

arachnopress 1.0.70

  • Added optional custom-styles.css for site-local styling
    • Automatically included by make build and make full; excluded from releases
    • This blog's opacity, corner, and LCARS-inspired theme demos use this file to avoid changing styles.css
  • Reorganised and cleaned-up the documentation, it was a bit fragmented!
  • Added configurable SVG background opacity
    • Choose 0-100% in steps of 10 from the Theme selector
    • Set the initial value with DEFAULT_BACKGROUND_OPACITY; default 30%
    • Opacity stays selected when changing themes or light/dark modes
Background opacity (%)

2026-09-20

arachnopress progress

There are a couple of minor tweaks I'd like to make around configurable background opacity, but other than that I think we are feature complete.

The last couple of updates have been the result of careful checks and targeting near-full POSIX compliance, the only exceptions being those I've deemed unlikely to cause any major problems and worth the exception (See requirements).

arachnopress 1.0.69

  • Minor fix to article slug string parsing
  • Fixed possible backslash in awk -v variable value (interpreted as escape sequence)
  • Minor awk script optimisations
  • Removed dependency on /dev/stderr (non-POSIX)

arachnopress 1.0.68

  • Applied fixes to gallery function for safe file path scanning and trailing slashes
  • Fixed a typography issue present in compact layouts when changing theme mode
  • Added a minor date validation fix
  • Updated documentation to reflect recent enhancements

2026-09-19

arachnopress 1.0.67

  • Added gallery function to image blocks
    • Existing image blocks become galleries by setting an image block's data-src to a sub-directory below the article directory
    • Gallery functionality is automated, so images don't need to be individually specified in article source
    • Supported images are arranged in filename order using a responsive grid of equal-sized thumbnails
    • Retained all existing image block controls for size, alignment, border, header, footer, and caption
    • Added filename tooltips, lazy loading, and asynchronous image decoding
    • Thumbnail images open the full image in the existing pop-over viewer with a raw-file link for direct viewing or download
Example GalleryPreview all 53 arachnopress theme variantsโ†“

2026-09-18

arachnopress 1.0.66

  • Introducing "Liquid Arachnopress" . . . Or something like that!
    • Added theme-independent Square, Soft, Rounded, and Round corner styles
    • Added DEFAULT_ROUNDING for the initial style, with dynamic controls in the Theme selector
    • Applied the selected style across navigation, controls, popovers, inline code, generated blocks, and images
    • Added a small separation between nested section and subsection highlights in the landscape index
Corner style

2026-09-16

Malazan Empire CUE files

Updated the Malazan CUE files to include Ian C. Esslemont's Novels of the Malazan Empire

  • Updated the zip archive (now includes Malazan Book of the Fallen 1-10 and Novels of the Malazan Empire 1-6)
  • The Novels of the Malazan Empire audiobooks actually had full-length chapter sections, but I verified and matched against Audnexus to generate the CUE files with correct section/chapter titles
  • The full-length chapters also exposed an oversight in the options provided to ffmpeg, which was updated in the m4b2aac.sh script (added -movie_timescale 1000)
  • The image of the Innioasis Y1 running Rockbox now shows the chapter breaks as provided by the CUE files
Downloadmalazan_cue_files.zipReady to use CUE files for MBotF / NotME17 KiBSHA256 (rockbox/malazan_cue_files.zip) = 81a4e1935bf3061e2944854d71281c9d0af0895269e49a52323d250152e69e9b

2026-09-13

arachnopress 1.0.65

  • Introducing four unofficial LCARS-inspired palette themes
    • RGB palette data sourced from Matthew Leonawicz's trekcolors project and used under its MIT licence
    • Added the upstream 2357, 2369, 2375, and 2379 palettes
    • Mapped the palette colours to arachnopress interface and syntax roles, with palette-coloured interface accents and tuned background opacity
  • These fan-inspired themes are not affiliated with or endorsed by CBS Studios Inc. or Paramount
  • The upstream trekcolors project provides additional palettes
LCARS-inspired

2026-09-11

VLC/tvOS samba config

Apparently VLC really doesn't like transitioning from a plain SMB session using server smb encrypt = if_required in [global] to an encrypted SMB share using server smb encrypt = required in [private].

The work-around for this is to use desired in [global] and continue using required in [private], so encryption is all or nothing and there's no renegotiation.

Now my LG Smart TV doesn't know how obsessed with 90s Star Trek I am (technically anything ≤2005, beyond that just isn't Star Trek. Also, the TV probably still knows!) - which reminds me, I must add an LCARS-inspired theme to arachnopress.

I also updated the smb.conf config file.

2026-09-10

Rockbox AAC-HE codec and Malazan CUE files

I've listened to Steven Erikson's Malazan Book of the Fallen audiobooks on a few devices, and annoyingly they have small embedded 20-25 minute sections rather than full chapter breaks, let alone chapter titles - I think this is because they are quite old and there were chapter length limitations in some audiobook platforms back in the 2010s.

Additionally to that, many audio players (including Rockbox) don't even support the embedded section markers.

I decided to fix it with CUE files, which are supported by Rockbox and many enthusiast DAP devices, but bumped into a couple of other issues along the way.

  • Limitations (Rockbox, but may apply to other devices)
    1. Supports <99 CUE sections per file, the Malazan audiobooks are massive and all contain >100 sections
    2. AAC-HE (High-Efficiency) codec misinterprets some timing table entries, causing incorrect FF/RW seeking and jumps to CUE chapter positions
    3. 32-bit timing range limitation, high sample-rate means lower permitted maximum playback duration
  • Fixes / Work-arounds
    1. Chapter counts are closer to ~24, and the (>100) section boundries always have a boundry that starts where a chapter begins
      • Used the Audnexus API to find real Section/Chapter times and titles for the 10 Malazan audiobooks
      • Matched each section/chapter timestamp against the already embedded sections breaks (extracted with ffprobe)
      • Created a ".chapters.txt" mapping file for each audiobook with chapter names and the index number of the embedded start section
      • Created m4b2cue.sh which builds a CUE file (Rockbox compatible) for each audiobook assisted by the mapping files
    2. The AAC-LC (Low-Complexity) codec doesn't suffer from the timing table entry issue
      • Created m4b2aac.sh which uses ffmpeg to transcode the original AAC-HE files to AAC-LC
        • Parallel processing was implemented because transcoding is bound to a single core, running 10 audiobooks in parallel took my M2 Max macbook about 40 minutes with fans running full speed!
        • Ensured album-art and existing metadata was preserved in the transcoded files
        • Added options for bitrate and channels, with sane defaults if unset.
    3. m4b2aac.sh uses a fixed 24kHz sample rate, which keeps below the 32-bit timing range limitation, providing a max of ~49.7 hours

After dropping the CUE files alongside the AAC-LC .m4b files and enabling the CUE file support setting in the Rockbox menu, everything worked perfectly with accurate FF/RW, chapter navigation, and chapter titles. I've provided the finished CUE files here, and included the chapter mapping files just in case they might be useful.

I'll update this to include Novels of the Malazan Empire as soon as I purchase them from libro.fm.

Downloadmalazan_cue_files.zipReady to use CUE files for MBotF / NotME17 KiBSHA256 (rockbox/malazan_cue_files.zip) = 81a4e1935bf3061e2944854d71281c9d0af0895269e49a52323d250152e69e9b

The next task is too see what can be fixed in the Rockbox source to remove these limitations . . .

Added to scripts article

These are POSIX sh scripts, and should be good templates for further (parallel) ffmpeg/ffprobe conversion/generator scripts.

Requirements are ffmpeg/ffprobe. m4b2cue.sh makes substantial use of jq.

  • m4b2aac.sh - Parallel batch Conversion of m4a/m4b to AAC-LC
  • m4b2cue.sh - Extract CUE file from m4a/m4b, optionally guided by Chapter metadata files

2026-09-06

Innioasis Y1 Rockbox

Innioasis Y1Y1 running Rockbox (AA for scale)rawโ†“

Flashed Rockbox to the Innioasis Y1 DAP . . .

The Innioasis Y1 is a relatively cheap android DAP, which seems to be based on mobile-type hardware - Importantly it has a relatively good speaker, which I believe is the sound-dampening ball filled type.

I bumped into it online whilst looking for a replacement Rockbox capable DAP to replace my ancient Sandisk Sansa Clip. I was also looking for something with an internal speaker, as I plan to use it for listening to audiobooks whilst going to sleep.

Flashing to Rockbox using rockbox-y1/rockbox was a painless process via the Mediatek Flash and Repair Utility - although I did end up doing it on debian, as macOS's raw USB access needed elevated privileges that I didn't want to provide (certainly not via sudo and a bunch of Python PyPI packages!)

The original firmware was pretty terrible in terms of sleep-timer configuration and fast-forward/rewind acceleration. Rockbox has both of these (and most everything else!) tuned/configurable to perfection - I have automatic resume and sleep timer enabled on boot, as well as bookmarking features.

The touch wheel works surprisingly well, and it looks like it can run Quake! Although, right now this is a dedicated Malazan Book of the Fallen device.

I recommend libro.fm for all your DRM-free audiobook needs.

2026-09-05

arachnopress 1.0.64

  • Enhancements to create/modify date handling
    • Added optional data-created and data-modified attributes to h2/h3 tags
    • Dates conditionally roll-up to parents and are used for better Atom feed ordering and article index sorting
    • A rule-based comparison mechanism is used for optimal handling of create/modify dates if/where provided

2026-09-04

arachnopress 1.0.63

  • Enhanced Atom feed with data-feed-sections article attribute values:
    • none (or unset), does not include article
    • h1, article title, plain-text summary, link to article
    • h2, "article title - section heading", link to section
    • h3, "article title - section heading", link to sub-section(s)

2026-09-03

arachnopress 1.0.62

  • Refined Atom 1.0 Feed support
  • โš›๏ธŽ button now has the correct mime-type (set for .atom files, and configured in httpd.conf example)

2026-09-02

arachnopress 1.0.61

  • Atom 1.0 Feed support added!
  • Article index browser-native tooltips with summary text and metadata
  • Article Creation/Updated dates now have default values and "auto" option
  • Theme mode selector configurable at build: off/exact/auto/light/dark

2026-08-30

Config files article

Added a config files article, as an archive for myself and maybe useful to a passing wanderer.

I think the universe wants me to join the drooling mass of smooth grey goo that is all TikTok and Meta-platform users, but I intend to persist into the next aeon by becoming such an obscenely supermassive black hole that, after a googol years of Hawking away on the cosmic back burner, my final evaporation crosses the conformal boundary as a Hawking point - thus remaining, in the weakest possible sense, available for the next universe.

2026-08-27

arachnopress 1.0.60

  • Added full article-pane size pop over viewers for image blocks

2026-08-25

Reading Tool

Porsche Design P8801Porsche Design Reading Tool P8801rawโ†“

Finally gave up struggling to read small-print . . .

  • Porsche Design Reading Tool P8801
    • Weight: ~16 g
    • Lens width: 48 mm
    • Lens height: ~21-22 mm
    • Bridge: ~21 mm
    • Temple: 150 mm
    • Overall width: ~126 mm
    • Front: stainless steel
    • Temples: high-tech polyamide / RXP
    • Lenses: polycarbonate (AR/UV)

Finding a decent pair of half-moon reading glasses that I can leave sat on the end of my nose all day turned out to be quite difficult until I found the Porsche Design "Reading Tool". I had to give up on my idea of actual half-moon lenses and gold plated frames for reading cosmic horror by candle light, but it still turned out well.

Apparently Porsche have been making this exact design for the last 20 years, which is pretty weird considering I could only find two videos of them on the less cosmic but certainly horrific YouTube. I can see why they stuck with it though, my first time needing glasses and I don't even notice they're there - I can actually read the tiny date window on my Seiko now, after forgetting it even had one!

2026-08-08

arachnopress 1.0.58

  • Image blocks needed a few enhancements, mainly alignment issues

arachnopress 1.0.59

  • It wasn't finished . . . ๐Ÿ˜‚

2026-08-07

Wristwatches

Seiko SPB315J1Prospex Heritage Turtle 1968 Re-Interpretationi (2022)rawโ†“

Aquired a new wristwatch . . .

  • Seiko's 6R35 movement
    • 70 hour power reserve
    • 21,600 vph / 3Hz / 6 bps
    • +25/-15 seconds daily (currently -10)
    • Durable and simple, but not particularly accurate
  • 200m water resistance
  • Screw-down crown
  • Super-Hard coating (DLC/Diamond-like carbon treated casing)
  • Unobtrusive date window between 4 and 5
  • Sapphire crystal, with underside AR coating

Very pleased with it, this was my first time adjusting a pin & collar bracelet, and I think the easiest so far to size - Lower end split pins need too much force (link tool / hammer) and higher end screw bar/tube systems always bother me due to tightening force and damaging the thread or screw-head (not forgetting decent screwdrivers).

arachnopress 1.0.57

  • I think it's finished . . .

Technical reference

arachnopress

Build a zero-JS static article site from hand-written HTML fragments.

DESCRIPTION

arachnopress builds a single-page or multi-page static site from articles/slug/article.html files. Run make build to generate site/, then serve it as static files or open site/index.html through file://.

The build runs in POSIX-like BSD, Linux, and macOS environments. The generated interface uses HTML and CSS only; it requires no client-side scripting or server-side processing.

FEATURES

  • File-backed code blocks with syntax highlighting, collapsible headers, optional footers, raw links, sizes, and header/footer navigation.
  • Download blocks with file sizes and checksums.
  • Image blocks and directory galleries with responsive sizing, framing, alignment, text flow, and enlarged-image popovers.
  • Small, editable HTML article fragments with optional arbitrary HTML.
  • Single-page or per-article output; optional unlisted articles and extensionless page links.
  • Responsive article, section, and subsection indexes; title/date sorting, summary tooltips, and creation/modification metadata.
  • Opt-in Atom entries for whole articles or sections, with subsection links.
  • Upstream colour palettes, Exact/Auto/Light/Dark modes, independent corner styles, and background-opacity controls.
  • SVG logos, favicons, and optional fixed or randomised themed backdrops; a single Unicode symbol provides the logo/favicon fallback.
  • Theme, contact, and license popovers; contact messages sent as marked GET requests for server-log collection.
  • No cookies or browser storage; optional release-archive packaging.

DOWNLOADS

Downloadarachnopress_1.0.70.tar.gzgzipped source archive317 KiBSHA256 (arachnopress_1.0.70.tar.gz) = 4a7495653e750d181cfa43f744cda49abb74022ef6aea181f9e8648c02c5473a

REQUIREMENTS

  • Build: POSIX-style sh, BSD or GNU make, standard Unix utilities, and mktemp.
  • Utilities: awk, sed, grep, sort, tr, date, dirname, pwd, printf, mkdir, rm, wc, cat, cksum, chmod, mv, cp, and find.
  • Release targets: cmp and tar with gzip support.
  • mktemp is widely available but is not specified by POSIX.
  • Optional highlighting: pygmentize (Pygments, default) or GNU source-highlight.
  • Optional download checksums: sha256, sha256sum, shasum, or openssl; the first successful one is used.
  • Browser: CSS :has(), :target, prefers-color-scheme, native details/summary, and HTML popovers. CSS random() is optional.

BUILD

  1. Work from the project root, which contains Makefile.
  2. Edit PUBLIC_SITE_ENV in the Makefile for the site's identity, appearance, and feed settings. Set its own SITE_URL and SITE_AUTHOR, or disable SITE_FEED. If removing the bundled article, replace its SVG paths or disable the SVG icon and backdrop.
  3. Add or edit articles below articles/.
  4. Run make build. Deploy or inspect site/index.html.
  • Environment and command-line make variables override profile defaults.
  • build copies all article directories and renders the site in staging before replacing site/.
  • Keep maintained files outside site/. Run builds from the source root, never from the generated tree.
  • Rerun after changing articles, assets, CSS, or settings. Stylesheet and favicon URLs carry content-based cache tokens.
  • All targets share site/; run only one build at a time.
Build examplesRun each example from the project rootraw
make build

# Set independent corner and background-opacity defaults.
make build DEFAULT_ROUNDING=rounded DEFAULT_BACKGROUND_OPACITY=20

# Per-article pages with system light/dark selection and no theme menu.
make build SITE_MODE=multi-page DEFAULT_THEME_MODE=auto \
    DEFAULT_THEME_SELECTOR=false

# Per-article files for file:// navigation, using a fixed theme.
make build SITE_MODE=multi-page SITE_URL_STYLE=html \
    DEFAULT_THEME=solarized-dark DEFAULT_THEME_MODE=off \
    DEFAULT_THEME_SELECTOR=false

# Published articles only, without theme controls or an article backdrop.
make build SITE_MODE_UNLISTED=false SITE_BACKGROUND=false \
    DEFAULT_THEME_MODE=off DEFAULT_THEME_SELECTOR=false

# Set this site's Atom feed identity.
make build SITE_FEED=true SITE_URL=https://example.com \
    SITE_AUTHOR='Example Author'

For generator release packaging, see MAINTENANCE.

ARTICLE FORMAT

Article structure

Create articles/slug/article.html with this structure:

articles/getting-started/article.htmlraw
<article class="article" id="getting-started" data-title="Getting Started" data-created="2026-07-09" data-modified="2026-07-09" data-feed-sections="h1">
  <header class="article-header">
    <p class="kicker">Guide</p>
    <h1>Getting Started</h1>
    <p class="summary">
      A short article built from one editable HTML fragment.
    </p>
  </header>

  <section>
    <h2>Example</h2>
    <p>Article text is normal HTML.</p>
    <h3>Subsection</h3>
    <p>Plain one-line h3 headings appear under their preceding section.</p>
  </section>
</article>
  • Keep the opening article tag on one line. Its class list must include article; id must match the directory name; data-title supplies the index title.
  • Use double-quoted attributes read by the generator.
  • The header contains h1, an optional kicker, and p class="summary". The summary may span lines and contain inline HTML.
  • Use normal HTML for article content. Author HTML and inline SVG are trusted input; they are not sanitised. Author-supplied scripts or external resources remain part of the output.
  • data-listed: true (default) or false; see Unlisted articles under CONFIGURATION.
  • data-feed-sections: none (default), h1, h2, or h3; see ATOM FEED.

Headings and links

  • Use h2 sections and h3 subsections. Every source h2/h3 is indexed; each must contain plain text and close on the same line. An h3 follows an h2.
  • Explicit heading IDs are optional. Otherwise, IDs derive from the article and heading text; repeated names receive unique suffixes.
  • Slugs and explicit IDs use ASCII letters, digits, dots, underscores, and hyphens. Article, heading, control, and generated-block IDs must not collide. Keep IDs on other author elements unique.
  • Single-page links use #article-id or #heading-id. Multi-page links use slug.html#heading-id, or slug#heading-id with extensionless URLs.
  • Copy section links from the generated index, or assign explicit IDs for links that must survive heading renames. Author-written links are not rewritten when the output mode changes.
Headings with optional IDs and datesraw
<h2 data-created="2026-07-09">BUILD</h2>
<h3>Requirements</h3>
<h3 id="custom-subsection" data-modified="2026-07-10">Explicit Subsection Anchor</h3>
<h2 id="custom-anchor" data-created="2026-07-09" data-modified="2026-07-10">Explicit Anchor</h2>

Dates

Article, h2, and h3 attributes accept auto or a value beginning with a valid YYYY-MM-DD date. Metadata, date sorting, and feed timestamps use the date portion.

Article data-created
Missing, empty, or auto: current UTC build date.
Article data-modified
Missing or empty: resolved creation date. auto: UTC build date.
Heading dates
data-modified takes precedence over data-created. auto uses the UTC build date. An undated h2 inherits the article's resolved modification date; an undated h3 contributes no date. A newer child h3 date updates its parent section's effective date.
  • The effective article modification date is the newest resolved article or heading date. It drives Updated metadata, modification sorting, and whole-article feed entries.
  • Missing and empty heading attributes have the same fallback behaviour.
  • The header displays Created, plus Updated when different, with middle-dot separators. Unlisted articles also display Unlisted.
  • Heading dates never reorder sections or subsections. Creation sorting continues to use the article's creation date.
  • Use explicit dates for stable publication metadata. The build does not write resolved dates or generated heading IDs into source files.

GENERATED BLOCKS

  • Each marker is an empty element on one source line.
  • data-src is relative to the article directory. Path components use ASCII letters, digits, dots, underscores, and hyphens; empty components, ., .., and symlinks are rejected.
  • Missing or invalid sources render visible missing blocks. Non-empty or multiline markers stop the build.
  • data-title overrides the displayed source path; data-note adds a header qualifier.

Code blocks

Code block markersraw
<pre class="code-block" data-src="src/example.c" data-title="example.c" data-note="A short C example" data-lang="c"></pre>
<pre class="code-block" data-src="build.sh" data-lang="sh" data-open="false" data-footer="false"></pre>
<pre class="code-block" data-src="output.txt" data-lang="text" data-header="false"></pre>
<pre class="code-block" data-src="output.txt" data-title="output.txt" data-lang="text" data-header="false" data-footer="true"></pre>
  • data-lang: explicit lexer, or omit for filename inference. Pygments auto also permits content guessing.
  • data-open="false": initially collapsed; default is open.
  • data-header="false": always visible, without a header.
  • data-footer="true" or "false": override automatic footer selection. Normally shown above CODE_FOOTER_LINES; headerless blocks require explicit true.
  • Header/footer arrows navigate between the bars. Raw links and sizes refer to the original file.

HIGHLIGHTER selects pygments (default), source-highlight, or none:

  • Missing tools, unknown lexers, failed highlighting, or unusable output fall back to escaped source. none always uses escaped source.
  • Source-highlight uses tools/html-fragment.outlang; an omitted or auto language enables its language inference.
  • Highlighted output follows the selected theme. Whitespace-only Pygments span.w and Source-highlight span.sh-normal wrappers are removed without removing whitespace.
Highlighter selectionraw
make build HIGHLIGHTER=none
make build HIGHLIGHTER=pygments
make build HIGHLIGHTER=source-highlight

Download blocks

Download block markerraw
<div class="article-downloads" data-src="release.tar.gz" data-title="release.tar.gz" data-note="Source archive"></div>
  • One data-src file per marker.
  • Displays the file size and, when available, its SHA256 checksum.
  • data-title changes the label, not the download path.

Image blocks

A file creates a single image; a directory creates a gallery. Both use the same size, alignment, frame, header, and footer options.

Image and gallery markersraw
<figure class="image-block" data-src="images/diagram.png" data-title="Diagram" data-note="Article-local image path" data-alt="Build flow diagram" data-caption="Article-local image"></figure>
<figure class="image-block" data-src="images/screenshots" data-title="Screenshots" data-alt="Installation screenshot" data-scale="large" data-border="fit"></figure>
<figure class="image-block" data-src="images/icon.png" data-title="Icon" data-alt="Small icon" data-scale="small" data-border="fit" data-align="start" data-header="false" data-footer="true"></figure>
<figure class="image-block" data-src="images/plain.png" data-alt="Borderless image" data-scale="small" data-border="none" data-align="end"></figure>
data-alt / data-caption
Alternative text / visible caption. A gallery caption covers the whole block; its alternative text prefixes each readable filename.
data-open / data-header
As for code blocks: open and headed by default; false collapses the block or removes its header.
data-footer
Shown with a header unless false. Headerless images require explicit true.
data-scale
small (20rem), medium (32rem), large (48rem), or full (default). Caps image/gallery width within available space; frame padding is additional.
data-border
full (default): full-width frame. fit: frame fitted to the image or gallery, with equal-width header and footer. none: no frame, padding, header, or footer; the block retains an opaque article-background surface.
data-align
start, center (default), or end. Aligns the image inside a full frame, or the entire fit/borderless block.
  • Single images preserve aspect ratio, scale down to fit, and do not upscale.
  • Images load lazily and decode asynchronously.
  • Unknown scale, border, or alignment values stop the build.

Galleries and viewers

  • Galleries read immediate AVIF, GIF, JPEG, PNG, SVG, and WebP files in filename order. They do not recurse; hidden, unsupported, or invalid filenames are ignored. An empty gallery renders a missing block.
  • Previews occupy equal square cells and may crop. Small galleries use one to three columns; larger galleries fit columns to the block width.
  • Thumbnail tooltips show filenames. The footer shows the image count; each enlarged gallery image has its own raw link.
  • Click an image to open its themed viewer over the article pane. The complete image retains its aspect ratio and scales down without upscaling.
  • Click the enlarged image or X, press Escape, or click outside to close. Image cursors indicate zoom in/out. Missing blocks have no viewer.

Text beside images

  1. Place the image marker directly inside an article section, before the text that should flow beside it.
  2. Use data-border="fit" or "none" with data-align="start" or "end".
  • Above 760px, the block occupies at most 55% of the section width, subject to its size limit. Following paragraphs and text lists flow beside it.
  • Headings, generated blocks, preformatted blocks, tables, horizontal rules, consecutive images, and the section end clear the flow.
  • Centered/full-frame blocks stay on their own row. At narrower widths, blocks return to normal flow and start/end-aligned images are centred.

CONFIGURATION

Edit Makefile defaults or override individual values with make build NAME=value.

  • PUBLIC_SITE_ENV: site defaults for build and full.
  • RELEASE_SITE_ENV: site defaults for release.
  • BUILD_ENV: shared defaults.
  • Below, public means build/full; release means the release profile. Boolean settings accept true or false.

Site and navigation

SITE_TITLE
Header brand, site page title, and contact-marker prefix. Default: public arachnogoat; release arachnopress.
SITE_MODE
single-page (default): all generated articles in index.html. multi-page: one article per slug.html, plus index.html containing the first listed article. Each page embeds its navigation rules and shares the CSS.
SITE_URL_STYLE
html: page links retain .html. extensionless: multi-page navigation and contact actions use slug and ./; files retain .html. Requires a server rewrite. Single-page output is unaffected. Default: public extensionless; release html.
ARTICLE_ORDER
Initial index order: title (default), created, or modified. Title sorting is case-insensitive; date sorting is newest first. Ties use the slug. Visitors can switch order with the index controls; sections and subsections retain source order.
  • The first listed article in the configured order is the initial article.
  • Multi-page slug.html titles use article title followed by SITE_TITLE; the index page uses SITE_TITLE.
  • Multi-page slugs cannot be index. Extensionless slugs also cannot contain dots or collide with a path at the site root.
  • Landscape navigation uses a left pane; narrow/portrait navigation uses horizontal article, section, and conditional subsection rows.
  • Article-title tooltips show a plain-text summary and a final line of Created/Updated dates. Unsupported summary markup falls back to dates only. Native tooltip appearance is browser-controlled.
  • Single-page HTML includes all generated article bodies and highlighted spans. Use multi-page output, excerpts, or HIGHLIGHTER=none for large code-heavy collections.

Unlisted articles

Mark an article data-listed="false". SITE_MODE_UNLISTED=true generates it; false excludes it from generated pages and navigation. Default: public true; release false.

  • Single-page: accessible by article/heading fragment, with section navigation, but absent from every article-title index.
  • Multi-page: its page indexes all listed articles plus itself, never another unlisted article. Listed pages omit it. Its robots directive is noindex, nofollow.
  • Unlisted content is public. Single-page output has no per-article robots directive. All included source directories are still copied to site/articles/, even when unlisted generation is disabled.
  • At least one listed article is required. Unlisted articles never appear in the Atom feed.

Themes and controls

DEFAULT_THEME
Theme input ID from tools/theme-menu.html, with or without theme-. Default: public everforest-hard-dark; release solarized-dark.
DEFAULT_THEME_MODE
off: no mode control; use the selected theme unchanged. exact, auto, light, or dark: show all four controls and select that initial mode. Default: public exact; release off.
DEFAULT_THEME_SELECTOR
true (default): show the Theme popover in either page mode. false: fix the theme, corners, and opacity to build values. The separate mode control remains governed by DEFAULT_THEME_MODE.
DEFAULT_ROUNDING
square, soft, rounded, or round. Applies across controls, highlights, blocks, images, and popovers. Default: public soft; release square.
DEFAULT_BACKGROUND_OPACITY
Initial SVG backdrop opacity: 0 to 100 percent in steps of 10. Default: 30. Its selector appears above Corners when SITE_BACKGROUND=true.
  • Exact uses the selected palette unchanged. Auto follows the browser's light/dark preference; Light and Dark force the mapped variant.
  • Corner style and background opacity are independent of theme/mode changes.
  • Selections persist within a single-page document. Reloading or opening another multi-page article restores build settings.
  • Set light/dark pairs with data-auto-light and data-auto-dark on theme inputs in tools/theme-menu.html. Supply both IDs, refer to existing themes, and include the source theme in its pair. Unmapped themes remain unchanged in every mode.
  • Palettes retain upstream RGB values. styles.css maps them to interface and syntax roles, without colour mixing or contrast adjustment.
Theme pair mappingExcerpt from tools/theme-menu.htmlraw
<input type="radio" name="theme" id="theme-solarized-dark" data-auto-dark="theme-solarized-dark" data-auto-light="theme-solarized-light" checked>
<label for="theme-solarized-dark">Dark</label>
<input type="radio" name="theme" id="theme-solarized-light" data-auto-dark="theme-solarized-dark" data-auto-light="theme-solarized-light">
<label for="theme-solarized-light">Light</label>

Logo and favicon

SITE_ICON_PATH
Custom SVG path relative to the build tree. Used as the header logo and copied unchanged to favicon.svg. Default: public articles/arachnopress/arachnogoatsundual.svg; release empty.
SITE_ICON_PATH_THEME
true: embed and theme the header SVG; requires SITE_ICON_PATH. false: display it as an external image. The favicon always keeps its original colours. Default: public true; release false.
SITE_ICON
Single Unicode symbol for the header and generated favicon when SITE_ICON_PATH is empty. Default: public U+4DEA; release U+4DD6.
SITE_ICON_COLOUR
Optional #rgb or #rrggbb override for the Unicode icon. Empty (default): header uses the article-title colour; generated favicon uses SVG's default text fill. Unused with a custom SVG.
  • Store SVGs below an included article, using the same path-component rules as generated blocks. Files must be readable, non-empty .svg files; do not use the generated favicon.svg.
  • For a themed logo, assign double-quoted class="colour-out" and class="colour-in" attributes to drawable elements or groups. Edit the SVG directly or use Inkscape's XML Editor.
  • CSS fills colour-out with the article background and colour-in with its title colour. The mapping follows the active theme and mode. Avoid inline style declarations that override those fills.
  • Restore the Unicode icon with SITE_ICON_PATH= and SITE_ICON_PATH_THEME=false.
Custom SVG ThemeExample custom SVG in Inkscape's XML editorโ†“
Custom SVG and Unicode icon buildsReplace article-slug after adding the SVGraw
# Custom SVG after adding the required classes.
make build SITE_ICON_PATH=articles/article-slug/site-icon.svg \
    SITE_ICON_PATH_THEME=true

# Restore the Unicode icon.
make build SITE_ICON_PATH= SITE_ICON_PATH_THEME=false

Article backdrop

SITE_BACKGROUND
Enable the fixed article-pane SVG background. Default: public true; release false.
SITE_BACKGROUND_PATH
SVG path relative to the build tree, with the same file and class requirements as a themed logo. Default: articles/arachnopress/arachnogoatsundual.svg.
SITE_BACKGROUND_RANDOM
true (default): browsers supporting CSS random() choose a scale and crop on document load. Unsupported browsers, or false, retain the SVG's base fit.
  • The backdrop paints colour-in with the theme's background artwork colour; colour-out is unpainted. DEFAULT_BACKGROUND_OPACITY controls opacity.
  • Use preserveAspectRatio="xMidYMid slice" for a centred cover crop, as in the bundled SVG.
  • Single-page navigation retains one backdrop; each multi-page document can receive a new random crop. Code, download, and image surfaces stay opaque.

Highlighting and local CSS

HIGHLIGHTER
pygments (default), source-highlight, or none. See Code blocks under GENERATED BLOCKS.
CODE_FOOTER_LINES
Non-negative source-line threshold for automatic code footers. Default: 23; blocks at or below it omit the footer.

Create custom-styles.css in the project root for site-local styling:

  • build and full copy it unchanged when present; release excludes it. It must be a readable regular file, not a symlink.
  • Pages load it after standard CSS and embedded navigation rules. Normal specificity applies; existing CSS variables follow the active theme.
  • Without the file, no custom stylesheet link is generated.

Feed identity

SITE_FEED
Generate feed.atom, discovery metadata, and the header's Atom link. Default: public true; release false.
SITE_URL
Canonical site root, including any deployment subdirectory. Required when the feed is enabled: absolute HTTP(S), with no whitespace, query, or fragment. Trailing slashes are removed. Default: public https://arachnogoat.com; release empty.
SITE_AUTHOR
Atom author name. Default: public musol@arachnogoat.com; release SITE_TITLE.

ATOM FEED

Enable SITE_FEED, set the feed identity, and opt listed articles in with data-feed-sections:

none
No entries; also used when the attribute is missing or empty.
h1
One entry with the article title, link, and available plain-text summary.
h2
One entry per section, titled "Article title - Section heading", with its section link as content.
h3
As h2, adding a linked list of that section's h3 headings.
  • h2/h3 fall back to h1 when the article has no sections. An h3 entry without child headings contains only its section link. Section entries do not include article prose.
  • Entry dates follow ARTICLE FORMAT: h1 uses the effective article modification date; section entries use their effective h2 date, including newer child dates.
  • An article-level auto date participates in whole-article entries. For section entries, it is only the fallback for an undated h2.
  • Dates use midnight UTC. Article groups sort by their newest entry; sections and linked subsections retain source order. The feed date is the newest entry date, or the UTC build date when empty.
  • Links follow SITE_MODE and SITE_URL_STYLE. IDs remain SITE_URL/#article-id or SITE_URL/#section-id, independent of dates and output mode.
  • Explicit heading IDs preserve feed identity across renames and reordering of duplicate headings. Changing SITE_URL changes IDs.
  • New sections create entries; edits and new h3 links retain the parent entry ID. Switching to section mode introduces existing sections as entries. Read/unread status is controlled by the reader.
  • The Atom 1.0 feed includes author, generator/version, HTML links, and the versioned site favicon. It may contain zero entries.

SERVING

  • Deploy the contents of site/ without changing relative paths.
  • For local file:// access, open site/index.html; multi-page navigation requires SITE_URL_STYLE=html.
  • Serve .atom as application/atom+xml.
  • Extensionless multi-page URLs require an internal rewrite from missing extensionless paths to their .html files.

The OpenBSD httpd(8) example supplies the Atom MIME type and rewrite. Replace its host, certificate, and document-root paths; validate before reloading. Keep specific location rules before the final rewrite.

OpenBSD httpd.conf examplerawโ†“
types {
	include "/usr/share/misc/mime.types"
	application/atom+xml atom
}

server "arachnogoat.com" {
	listen on * port 80

	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}

	location * {
		block return 301 "https://$HTTP_HOST$REQUEST_URI"
	}
}

server "arachnogoat.com" {
	listen on * tls port 443

	tls {
		certificate "/etc/ssl/arachnogoat.com.fullchain.pem"
		key "/etc/ssl/private/arachnogoat.com.key"
	}

	root "/htdocs/arachnogoat.com"

	# Required when arachnopress uses SITE_URL_STYLE=extensionless.
	location not found match "/[^./]+$" {
		request rewrite "$DOCUMENT_URI.html"
	}
}

Contact messages

  • The Contact popover submits a GET to the current page and displays a confirmation. It requires HTTP(S); use HTTPS for transport encryption.
  • reply: optional address, maximum 254 characters. message: required, maximum 500 characters.
  • The first query item is sanitised-site-title_contact=1. The title is lowercased; runs outside ASCII letters, digits, dots, underscores, and hyphens become an underscore; edge underscores are removed. A result without letters or digits becomes arachnopress.
  • The default markers are arachnogoat_contact=1 for public builds and arachnopress_contact=1 for releases.
  • Configure access logging to retain query strings. Extract and process marked requests from the log; the site has no separate delivery or storage service.
  • Messages remain in URLs, browser history, and server logs. Do not include confidential information.
Contact requestBrowser-encoded query valuesraw
Browser URL with SITE_TITLE="Example Site":
https://host.example/?example_site_contact=1&reply=operator%40example.com&message=Short+message#contact-confirmation

HTTP request target logged by a server that retains query strings:
GET /?example_site_contact=1&reply=operator%40example.com&message=Short+message

MAINTENANCE

Generator releases

These targets package the generator; routine site updates use make build.

GENERATOR_LABEL
Generator name displayed in site and Atom metadata. Default: arachnopress; archive names remain arachnopress.
GENERATOR_VERSION
Dot-separated digits used in metadata and archive names. Default: 1.0.70.
make release
Builds only articles/arachnopress with RELEASE_FILES, leaving a working release tree in site/. Creates arachnopress_VERSION.tar.gz at the project root, under a matching top-level directory. Its own download is absent inside that tree/archive.
make full
Requires the matching root release archive. Builds all articles with RELEASE_FILES and installs the archive into the staged generator article. Leaves that complete tree in site/.
  • The generator article must contain exactly one data-release="generator" download marker.
  • Both targets update that complete marker line to the current archive name and remove arachnopress_*.tar.gz from the source generator article directory after publication. Other root archives remain.
  • build uses RUNTIME_FILES and all articles; it leaves source download markers and archives unchanged.
  1. Update GENERATOR_VERSION and run make release.
  2. Copy the root archive into articles/arachnopress/, then run make build to publish it with other articles. Alternatively, make full integrates it into staged output.

Failures and cleanup

  • A validation, staging, or rendering failure preserves the previous site/. Publication replaces the old output directory.
  • Targets exit zero on success and non-zero on invalid input or an unhandled command/filesystem failure. Correct the diagnostic and rerun.
  • Missing block sources, optional highlighting, and unavailable checksums are non-fatal: blocks show missing-source notices, escaped code, or no checksum.
  • Normal exits/signals clean temporary files. After an untrappable termination, stop all builds and inspect .site-build.* under the project root and static-site-build.* under TMPDIR before removing leftovers.
  • There is no clean target. Remove site/ to discard generated output; remove obsolete root archives by exact filename.
  • Inclusion lists reject symlinks, including those within article directories.
Remove generated outputraw
# Run from the project root when no build is running.
rm -rf site
Missing release archive
Run make release with the same version before make full, from the same source root.
No listed articles found
Provide at least one article with data-listed="true" or no data-listed attribute.
Invalid theme, SVG, heading, marker, or path
Check the named input against CONFIGURATION, ARTICLE FORMAT, or GENERATED BLOCKS. Diagnostics identify the rejected value or source.

Build environment

PATH
Locates build utilities, highlighters, and checksum programs.
TMPDIR
Parent of private static-site-build.* rendering directories; default /tmp. Target staging uses .site-build.* beneath the source root.
LC_ALL / SOURCE_HIGHLIGHT_DATADIR
The scripts set LC_ALL=C; the generator unsets SOURCE_HIGHLIGHT_DATADIR.
  • tools/build-profile.sh invokes tools/build.sh with internal BUILD_ROOT and ARTICLE_SELECTION settings.
  • Direct generator execution writes into its build root, defaults to the source root, and does not stage or package site/.
  • Direct execution uses release-style site defaults and the script version above; it reads custom-styles.css from its build root.

FILES

Maintained inputs

Makefile
Targets, profile defaults, and inclusion lists.
articles/slug/article.html / articles/slug/*
Article fragments and their code, image, gallery, and download assets.
styles.css / custom-styles.css
Shared layout, palettes, and syntax roles / optional site-local overrides.
tools/build.sh / tools/build-profile.sh
Article renderer / target staging and packaging.
tools/theme-menu.html
Theme controls and light/dark pair mappings.
tools/html-fragment.outlang
Source-highlight HTML-fragment output definition.
tools/license.txt / THIRD_PARTY_NOTICES.txt / licenses/
Static project license / palette sources and retained third-party notices.
README.arachnopress
Plain-text edition of this reference.

Generated output

site/index.html / site/slug.html
Entry page / additional multi-page articles, with embedded navigation rules.
site/articles/
Copied article directories, including source HTML and published assets.
site/styles.css / site/custom-styles.css
Copied shared stylesheet / optional custom stylesheet.
site/theme-auto.css
Derived theme-mode rules; present when the mode control is enabled and a selectable theme has a light/dark mapping.
site/favicon.svg / site/feed.atom
Generated or copied favicon / optional Atom feed.
site/THIRD_PARTY_NOTICES.txt / site/licenses/
Copied notices required by the published site.
Source tree and routine build outputrawโ†“
.
|-- Makefile
|-- README.arachnopress
|-- THIRD_PARTY_NOTICES.txt
|-- articles
|   `-- arachnopress
|       |-- article.html
|       `-- ...
|-- custom-styles.css (optional)
|-- licenses
|   |-- mit.txt
|   `-- oksolar-cc0.txt
|-- site
|   |-- THIRD_PARTY_NOTICES.txt
|   |-- articles
|   |   `-- arachnopress
|   |       |-- article.html
|   |       `-- ...
|   |-- custom-styles.css (optional)
|   |-- favicon.svg
|   |-- feed.atom (when enabled)
|   |-- index.html
|   |-- licenses
|   |   |-- mit.txt
|   |   `-- oksolar-cc0.txt
|   |-- styles.css
|   `-- theme-auto.css (when mapped modes are enabled)
|-- styles.css
`-- tools
    |-- build-profile.sh
    |-- build.sh
    |-- html-fragment.outlang
    |-- license.txt
    `-- theme-menu.html
Source tree and routine build output749 Brawโ†‘

LICENSE

  • arachnopress: BSD 3-Clause; see tools/license.txt.
  • The license popover embeds that file as escaped text. Its first non-blank line supplies the title; the text is otherwise static.
  • Bundled palettes retain their upstream licences; see THIRD_PARTY_NOTICES.txt and licenses/. The license and Theme popovers link to those notices.

Patch Notes

bsddd.rd

OpenBSD/amd64 parallel disk-overwrite ramdisk and miniroot targets.

Downloadbsddd.rd.patchApplies below /usr/src27 KiBSHA256 (bsddd.rd.patch) = baefc54163b41f43799a6379ee7606a9bc6bb7affe66304f91c071175b24480d

DESCRIPTION

The patch adds separate bsddd.rd and minirootXX_dd.img targets. The ramdisk waits for one eligible sd(4) or wd(4) disk to disappear, then overwrites the remaining disks concurrently. Two console confirmations or an optional timeout instead select every eligible attached disk.

/dev/zero is the default input. /dev/urandom is selectable at build time. Console messages and interval-based bells report status.

The patch adds files only. Stock source files, Makefiles, bsd.rd, minirootXX.img, and cdXX.iso targets are unchanged. Enhanced targets are available only through Makefile.bsddd.

INTENDED USE

bsddd.rd supports batch whole-disk overwriting when preparing decommissioned systems for disposal or reuse, particularly where device-native sanitize or secure-erase facilities are unavailable or impractical. It runs from RAM, allowing boot media and optional console equipment to be moved between machines while wipes continue.

PATCH

The aggregate patch contains the complete build overlay and README.

bsddd.rd.patchrawโ†“
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/bsddd/Makefile.bsddd
@@ -0,0 +1,233 @@
+#	$OpenBSD$
+
+.include <bsd.own.mk>
+
+# Stock ramdisk_cd outer-image size, retained as an override point.
+BSDDD_FSSIZE?=	11392
+BSDDD_DD_IF?=	/dev/zero
+BSDDD_WIPE_TIMEOUT?=	0
+
+BSDDD_BASEDIR=	${.CURDIR}/..
+BSDDD_TOP=	${.CURDIR}/../../../..
+BSDDD_UTILS=	${BSDDD_BASEDIR}/../../miniroot
+BSDDD_MTREE=	${BSDDD_UTILS}/mtree.conf
+BSDDD_EFIBOOT=	${DESTDIR}/usr/mdec/BOOTX64.EFI \
+		${DESTDIR}/usr/mdec/BOOTIA32.EFI
+BSDDD_MOUNT_ARGS_MSDOS=	-o-s
+
+BSDDD_RAMDISK=	RAMDISK_CD_DD
+BSDDD_KERNEL=	${.OBJDIR}/bsd
+BSDDD_FS=	miniroot${OSrev}_dd.img
+BSDDD_BUILDOBJDIR=	${.OBJDIR}/build
+BSDDD_KERNELOBJDIR=	${BSDDD_BUILDOBJDIR}/kernel/${BSDDD_RAMDISK}
+BSDDD_INSTBIN=	${BSDDD_BUILDOBJDIR}/instbin
+BSDDD_RDOBJDIR=	${.OBJDIR}/rdobj
+BSDDD_MOUNT_POINT=	${.OBJDIR}/mnt
+BSDDD_VND=	${.OBJDIR}/vnd
+BSDDD_BOOT=	${.OBJDIR}/boot
+BSDDD_OBJCHECK=	${.OBJDIR}/.bsddd-obj-ok
+BSDDD_MAKEFILE=	${.CURDIR}/Makefile.bsddd
+BSDDD_KERNELCONF=	${.CURDIR}/${BSDDD_RAMDISK}
+BSDDD_BASECONF=	${BSDDD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD
+BSDDD_KERNELCONFDEPS=	${BSDDD_BASECONF}
+BSDDD_LISTS=	${BSDDD_BASEDIR}/list ${.CURDIR}/list.dd
+BSDDD_SCRIPTSRC=	${.CURDIR}/bsddd
+BSDDD_SCRIPT=	${BSDDD_RDOBJDIR}/bsddd
+BSDDD_PROFILE=	${.CURDIR}/dot.profile
+# Stock 7360-block rdroot; keep paired with RAMDISK_CD_DD MINIROOTSIZE.
+BSDDD_RDROOT_SIZE?=	3680k
+BSDDD_MRMAKEFSARGS?=	-s ${BSDDD_RDROOT_SIZE} \
+		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
+
+.PHONY: bsddd
+bsddd: bsddd.rd ${BSDDD_FS}
+
+${BSDDD_OBJCHECK}:
+	@if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \
+		echo "private object directory is not active;" >&2; \
+		echo "run 'make -f Makefile.bsddd obj' separately first" >&2; \
+		exit 1; \
+	fi
+	touch $@
+
+${BSDDD_KERNEL}: ${BSDDD_OBJCHECK} ${BSDDD_MAKEFILE} \
+	    ${BSDDD_KERNELCONF} ${BSDDD_KERNELCONFDEPS}
+	install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${BSDDD_KERNELOBJDIR}
+	su ${BUILDUSER} -c \
+	    'config -b ${BSDDD_KERNELOBJDIR} -s ${BSDDD_TOP}/sys \
+	    ${BSDDD_KERNELCONF} && cd ${BSDDD_KERNELOBJDIR} && \
+	    MAKEOBJDIR=${BSDDD_KERNELOBJDIR} ${MAKE} clean && \
+	    exec env MAKEOBJDIR=${BSDDD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'
+	cp -p ${BSDDD_KERNELOBJDIR}/bsd $@
+
+bsddd.gz: bsddd.rd
+	objcopy -g -x -R .comment -R .SUNW_ctf \
+	    -K rd_root_size -K rd_root_image \
+	    bsddd.rd bsddd.strip
+	gzip -9cn bsddd.strip > bsddd.gz
+
+.PHONY: bsddd-stock-deps bsddd-instbin
+bsddd-stock-deps: ${BSDDD_OBJCHECK} ${BSDDD_MAKEFILE}
+	@_objroot=`cd ${BSDDD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \
+	if [ ! -d "$$_objroot" ]; then \
+		echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \
+		echo "create it before building bsddd" >&2; \
+		exit 1; \
+	fi
+	cd ${BSDDD_TOP}/lib && ${MAKE} obj
+	cd ${BSDDD_TOP}/distrib/special && ${MAKE} obj
+	cd ${BSDDD_BASEDIR} && ${MAKE} obj
+	@_srcdir=`cd ${BSDDD_BASEDIR} && pwd`; \
+	_objdir=`cd ${BSDDD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
+	if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \
+		echo "normal ramdisk_cd object directory is not active" >&2; \
+		exit 1; \
+	fi
+	cd ${BSDDD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}
+
+bsddd-instbin: bsddd-stock-deps
+	cd ${BSDDD_BASEDIR} && ${MAKE} ${MFLAGS} instbin
+	install -d ${BSDDD_BUILDOBJDIR}
+	@_objdir=`cd ${BSDDD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
+	if [ ! -f "$$_objdir/instbin" ]; then \
+		echo "stock ramdisk_cd instbin was not built" >&2; \
+		exit 1; \
+	fi; \
+	cp -p "$$_objdir/instbin" ${BSDDD_INSTBIN}
+
+bsddd.rd: bsddd-files bsddd-instbin ${BSDDD_KERNEL}
+	install -d ${BSDDD_RDOBJDIR}
+	rm -f ${BSDDD_RDOBJDIR}/instbin \
+	    ${BSDDD_RDOBJDIR}/mr.fs \
+	    ${BSDDD_RDOBJDIR}/bsd.rd
+	cp -p ${BSDDD_INSTBIN} ${BSDDD_RDOBJDIR}/instbin
+	rm -rf ${BSDDD_RDOBJDIR}/mr.fs.d
+	install -d -o root -g wheel ${BSDDD_RDOBJDIR}/mr.fs.d
+	mtree -def ${BSDDD_MTREE} -p ${BSDDD_RDOBJDIR}/mr.fs.d -u
+	CURDIR=${BSDDD_BASEDIR} OBJDIR=${BSDDD_RDOBJDIR} OSrev=${OSrev} \
+	    TARGDIR=${BSDDD_RDOBJDIR}/mr.fs.d UTILS=${BSDDD_UTILS} \
+	    RELEASEDIR=${RELEASEDIR} sh ${BSDDD_UTILS}/runlist.sh \
+	    ${BSDDD_LISTS}
+	rm ${BSDDD_RDOBJDIR}/mr.fs.d/instbin
+	makefs ${BSDDD_MRMAKEFSARGS} ${BSDDD_RDOBJDIR}/mr.fs \
+	    ${BSDDD_RDOBJDIR}/mr.fs.d
+	cp -p ${BSDDD_KERNEL} ${BSDDD_RDOBJDIR}/bsd.rd
+	rdsetroot ${BSDDD_RDOBJDIR}/bsd.rd ${BSDDD_RDOBJDIR}/mr.fs
+	cp ${BSDDD_RDOBJDIR}/bsd.rd $@
+
+${BSDDD_FS}: bsddd.gz
+	-umount -f ${BSDDD_MOUNT_POINT} >/dev/null 2>&1
+	@if [ -e ${BSDDD_VND} ] && [ ! -s ${BSDDD_VND} ]; then \
+		rm -f ${BSDDD_VND}; \
+	fi
+	@if [ -e ${BSDDD_VND} ]; then \
+		echo "stale private vnd state; run" \
+		    "'make -f Makefile.bsddd unconfig-bsddd' first" >&2; \
+		exit 1; \
+	fi
+	install -d ${BSDDD_MOUNT_POINT}
+	dd if=/dev/zero of=${BSDDD_FS} bs=512 count=${BSDDD_FSSIZE}
+	vnconfig -v ${.OBJDIR}/${BSDDD_FS} > ${BSDDD_VND}
+	fdisk -yi -l ${BSDDD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
+	    `cat ${BSDDD_VND}`
+	echo '/ *' | disklabel -wAT- `cat ${BSDDD_VND}`
+	newfs -t msdos /dev/r`cat ${BSDDD_VND}`i
+	mount ${BSDDD_MOUNT_ARGS_MSDOS} /dev/`cat ${BSDDD_VND}`i \
+	    ${BSDDD_MOUNT_POINT}
+	mkdir -p ${BSDDD_MOUNT_POINT}/efi/boot
+	cp ${BSDDD_EFIBOOT} ${BSDDD_MOUNT_POINT}/efi/boot
+	umount ${BSDDD_MOUNT_POINT}
+	newfs -O 1 -m 0 -o space -i 524288 -c ${BSDDD_FSSIZE} \
+	    /dev/r`cat ${BSDDD_VND}`a
+	mount /dev/`cat ${BSDDD_VND}`a ${BSDDD_MOUNT_POINT}
+	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${BSDDD_BOOT}
+	installboot -v -r ${BSDDD_MOUNT_POINT} `cat ${BSDDD_VND}` \
+	    ${DESTDIR}/usr/mdec/biosboot ${BSDDD_BOOT}
+	install -c -m 555 -o root -g wheel bsddd.gz \
+	    ${BSDDD_MOUNT_POINT}/bsd
+	df -i ${BSDDD_MOUNT_POINT}
+	umount ${BSDDD_MOUNT_POINT}
+	vnconfig -u `cat ${BSDDD_VND}`
+	rm -f ${BSDDD_VND}
+
+.PHONY: bsddd-check bsddd-files
+bsddd-check:
+	@case "${BSDDD_DD_IF}" in \
+	/dev/zero|/dev/urandom) ;; \
+	*) echo "BSDDD_DD_IF must be /dev/zero or /dev/urandom" >&2; exit 1;; \
+	esac
+	@case "${BSDDD_WIPE_TIMEOUT}" in \
+	''|*[!0-9]*|0[0-9]*) \
+		echo "BSDDD_WIPE_TIMEOUT must be 0 or a positive decimal integer" >&2; \
+		exit 1;; \
+	esac
+	@if ! [ "${BSDDD_WIPE_TIMEOUT}" -le 2147483647 ]; then \
+		echo "BSDDD_WIPE_TIMEOUT exceeds 2147483647" >&2; \
+		exit 1; \
+	fi
+	test -s ${BSDDD_SCRIPTSRC}
+	test -s ${BSDDD_PROFILE}
+	test -s ${.CURDIR}/list.dd
+
+bsddd-files: ${BSDDD_OBJCHECK} bsddd-check ${BSDDD_SCRIPTSRC} \
+	    ${BSDDD_PROFILE} ${.CURDIR}/list.dd
+	install -d ${BSDDD_RDOBJDIR}
+	rm -f ${BSDDD_SCRIPT}.tmp
+	sed -e 's|^DD_IF=.*|DD_IF=${BSDDD_DD_IF}|' \
+	    -e 's|^WIPE_TIMEOUT=.*|WIPE_TIMEOUT=${BSDDD_WIPE_TIMEOUT}|' \
+	    ${BSDDD_SCRIPTSRC} > \
+	    ${BSDDD_SCRIPT}.tmp
+	grep -Fqx 'DD_IF=${BSDDD_DD_IF}' ${BSDDD_SCRIPT}.tmp
+	grep -Fqx 'WIPE_TIMEOUT=${BSDDD_WIPE_TIMEOUT}' ${BSDDD_SCRIPT}.tmp
+	mv ${BSDDD_SCRIPT}.tmp ${BSDDD_SCRIPT}
+
+.PHONY: unconfig-bsddd
+unconfig-bsddd:
+	-umount -f ${BSDDD_MOUNT_POINT} >/dev/null 2>&1
+	@if [ -e ${BSDDD_VND} ] && [ ! -s ${BSDDD_VND} ]; then \
+		rm -f ${BSDDD_VND}; \
+	elif [ -f ${BSDDD_VND} ]; then \
+		_vnd=`cat ${BSDDD_VND}`; \
+		_unit=$${_vnd#vnd}; \
+		if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \
+			echo "invalid private vnd state: $$_vnd" >&2; exit 1; \
+		fi; \
+		case "$$_unit" in \
+		*[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \
+		esac; \
+		_info=`vnconfig -l "$$_vnd"` || exit 1; \
+		case "$$_info" in \
+		"$$_vnd: not in use") rm -f ${BSDDD_VND} ;; \
+		"$$_vnd: covering ${.OBJDIR}/${BSDDD_FS} on "*) \
+			vnconfig -u "$$_vnd" && rm -f ${BSDDD_VND} ;; \
+		*) echo "refusing to detach vnd not owned by bsddd: $$_info" >&2; \
+			exit 1 ;; \
+		esac; \
+	fi
+
+.ifdef RELEASEDIR
+.PHONY: install-bsddd
+install-bsddd: bsddd.gz ${BSDDD_FS}
+	cp bsddd.gz ${RELEASEDIR}/bsddd.rd
+	cp ${BSDDD_FS} ${RELEASEDIR}
+	chmod a+r ${RELEASEDIR}/bsddd.rd ${RELEASEDIR}/${BSDDD_FS}
+.endif
+
+.PHONY: clean-bsddd clean cleandir
+clean-bsddd: unconfig-bsddd
+	rm -f bsddd.rd bsddd.gz bsddd.strip ${BSDDD_FS} \
+	    ${BSDDD_KERNEL} ${BSDDD_BOOT} ${BSDDD_OBJCHECK}
+	rm -rf ${BSDDD_BUILDOBJDIR} ${BSDDD_RDOBJDIR}
+	-rmdir ${BSDDD_MOUNT_POINT}
+
+clean cleandir: clean-bsddd
+
+.PHONY: prepare-unpatch-bsddd
+prepare-unpatch-bsddd: clean-bsddd
+	@if [ -L ${.CURDIR}/obj ]; then \
+		_obj=`readlink ${.CURDIR}/obj`; \
+		echo "empty private object directory may be removed: $$_obj"; \
+		rm -f ${.CURDIR}/obj; \
+	fi
+
+.include <bsd.obj.mk>
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/bsddd/RAMDISK_CD_DD
@@ -0,0 +1,12 @@
+#	$OpenBSD$
+
+include "arch/amd64/conf/RAMDISK_CD"
+
+# Retain an explicit, stock-sized rdroot override for later adjustment.
+option		BSDDD_RDROOT
+rmoption	MINIROOTSIZE
+option		MINIROOTSIZE=7360
+rmoption	BSDDD_RDROOT
+
+pcppi0	at isa?
+spkr0	at pcppi?
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/bsddd/README.bsddd
@@ -0,0 +1,238 @@
+BSDDD.RD                    bsddd.rd build notes                    BSDDD.RD
+
+NAME
+     bsddd.rd - OpenBSD/amd64 parallel disk-overwrite ramdisk
+
+DESCRIPTION
+     The patch adds separate bsddd.rd and minirootXX_dd.img targets.  The
+     ramdisk waits for one eligible sd(4) or wd(4) disk to disappear, then
+     overwrites the remaining disks concurrently.  Two console confirmations
+     or an optional timeout instead select every eligible attached disk.
+
+     /dev/zero is the default input.  /dev/urandom is selectable at build
+     time.  Console messages and interval-based bells report status.
+
+     The patch adds files only.  Stock source files, Makefiles, bsd.rd,
+     minirootXX.img, and cdXX.iso targets are unchanged.  The enhanced targets
+     are available only through Makefile.bsddd.
+
+INTENDED USE
+     bsddd.rd supports batch whole-disk overwriting when preparing
+     decommissioned systems for disposal or reuse, particularly where
+     device-native sanitize or secure-erase facilities are unavailable or
+     impractical.  It runs from RAM, allowing boot media and optional console
+     equipment to be moved between machines while wipes continue.
+
+SYNOPSIS
+     Build on the matching OpenBSD/amd64 release after completing the normal
+     source and release prerequisites.  BSDOBJDIR and the amd64 boot files
+     below ${DESTDIR}/usr/mdec must exist.  The documented commands require
+     root privileges.
+
+     Apply the patch below /usr/src, create the private object link, then build:
+
+           cd /usr/src
+           patch -p1 < /root/bsddd.rd.patch
+           cd distrib/amd64/ramdisk_cd/bsddd
+           make -f Makefile.bsddd obj
+           make -f Makefile.bsddd
+
+     Select pseudorandom input instead:
+
+           make -f Makefile.bsddd BSDDD_DD_IF=/dev/urandom
+
+     Enable a 300-second all-disk timeout:
+
+           make -f Makefile.bsddd BSDDD_WIPE_TIMEOUT=300
+
+     Install release artifacts when RELEASEDIR is set:
+
+           make -f Makefile.bsddd install-bsddd \
+               RELEASEDIR=/path/to/release
+
+TARGETS
+     bsddd
+             Builds bsddd.rd and minirootXX_dd.img.
+
+     install-bsddd
+             Defined when RELEASEDIR is set.  Installs compressed bsddd.rd and
+             minirootXX_dd.img.
+
+     unconfig-bsddd
+             Recovers private mount and vnd(4) state after an interrupted
+             media build.  An unowned vnd(4) is not detached.
+
+     clean-bsddd, clean, cleandir
+             Remove private outputs.  Stock instbin objects remain under
+             normal OpenBSD clean ownership.
+
+     prepare-unpatch-bsddd
+             Cleans private outputs, prints the private object path, and
+             removes the source obj symlink.
+
+BUILD MODEL
+     make obj creates a private object link below BSDOBJDIR.  The custom
+     kernel, rdroot, instbin copy, and media objects remain below that path.
+
+     The wrapper builds stock ramdisk_cd instbin objects in their normal object
+     directories, then copies instbin into private state.  Stock source,
+     configuration, installed use, and cleanup ownership are unchanged.  Do
+     not run bsddd concurrently with a build or clean using those objects.
+
+     The private rdroot replaces .profile, installs /bsddd, and creates
+     /dev/speaker.  It adds no userland binaries.  The private kernel retains
+     the stock rdroot size and adds pcppi(4) and speaker(4).
+
+     No parent Makefile or SUBDIR list is changed.  Normal builds and cleans do
+     not enter the bsddd source directory or its private objects.
+
+DISK SELECTION
+     /bsddd reads hw.disknames with sysctl(8) and selects names matching
+     sd[0-9]* and wd[0-9]*.  This includes softraid(4) volumes, their backing
+     disks, and disks presented through common amd64 storage controllers.
+
+     Wiping begins when the current set equals the initial set minus exactly
+     one name.  Other changes leave the ramdisk waiting.  The accepted set is
+     frozen; later arrivals are excluded.  The interlock cannot identify the
+     boot medium.  Two explicit y responses or timeout expiry select every
+     eligible attached disk.  The timer continues during both prompts.
+
+     Other device classes are excluded.  After the interlock, /bsddd detaches
+     auto-assembled softraid(4) volumes and removes their names from the set.
+     Their backing disks remain selected.  Inspection or detach failure stops
+     before writes.
+
+     MAKEDEV(8) creates device nodes.  disklabel(8) supplies sector size and
+     total sectors.  dd(1) writes each whole-disk raw character device in 1 MiB
+     transfers followed by an exact sector-sized tail.  Disks run concurrently.
+     One device failure does not stop other jobs.  A status failure waits for
+     active jobs.  sync(8) precedes the final result.
+
+FEEDBACK
+     Console-bell counts and intervals identify each state:
+
+           three short bells        waiting without a timeout
+           four rapid bells         waiting with an all-disk timeout
+           two separated bells      disk writes started
+           one bell                 wipe progress
+           five short bells         all selected disks completed
+           three separated bells    setup, status, or disk-write failure
+
+     Ready patterns repeat every 10 seconds.  Completion and failure patterns
+     repeat every 30 seconds.  Progress is reported every 60 seconds.  A
+     console bell is used if speaker output fails.  The result loop continues
+     until power-off or reset.
+
+USE
+     Write minirootXX_dd.img to removable media.  This replaces the selected
+     device's partition table and filesystems.  Verify the output device.  For
+     example, as root on OpenBSD:
+
+           dd if=./miniroot79_dd.img of=/dev/rsdXc bs=1m
+           sync
+
+     Boot the target.  At the ready pattern, remove the boot medium.  Removal
+     of any eligible disk satisfies the name-based interlock.  Two separated
+     bells mark write startup.  Wait for the repeating completion pattern
+     before power-off.  Stock amd64 BIOS and UEFI boot programs are used.
+     Secure Boot is unsupported.
+
+     A boot loader or matching OpenBSD/amd64 pxeboot(8) may load the compressed
+     bsddd.rd directly.  Without removable media, use the console confirmations.
+
+REBUILD AND CLEAN
+     Rebuild private outputs:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/bsddd
+           make -f Makefile.bsddd clean
+           make -f Makefile.bsddd
+
+     Recover an interrupted media build first:
+
+           make -f Makefile.bsddd unconfig-bsddd
+
+     Before reversing the patch:
+
+           make -f Makefile.bsddd prepare-unpatch-bsddd
+           cd /usr/src
+           patch -R -p1 < /root/bsddd.rd.patch
+           rmdir distrib/amd64/ramdisk_cd/bsddd
+
+VARIABLES
+     BSDDD_DD_IF
+             Build-time dd(1) input device.  /dev/zero is the default.
+             /dev/urandom supplies non-blocking pseudorandom data but may limit
+             throughput.  No other value is accepted.
+
+     BSDDD_WIPE_TIMEOUT
+             Build-time automatic all-disk deadline in seconds.  The default,
+             0, disables it.  Values from 1 through 2147483647 enable it.
+             Expiry selects every eligible attached disk, including boot media.
+
+     BSDDD_FSSIZE
+             Outer miniroot size in 512-byte blocks.  Default: 11392, matching
+             stock ramdisk_cd.
+
+     BSDDD_RDROOT_SIZE
+             Private rdroot size.  Default: 3680k, matching 7360 512-byte
+             blocks.  An override must match MINIROOTSIZE in RAMDISK_CD_DD.
+
+     BSDDD_MRMAKEFSARGS
+             makefs(8) arguments for the private rdroot.  The default uses
+             BSDDD_RDROOT_SIZE and stock filesystem parameters.
+
+     BSDOBJDIR
+             Normal OpenBSD object root and base for the private obj symlink.
+             Default: /usr/obj.
+
+     DESTDIR
+             Optional prefix for installed amd64 boot files under usr/mdec.
+             Default: empty.
+
+     RELEASEDIR
+             Enables install-bsddd and names its destination directory.
+
+FILES
+     distrib/amd64/ramdisk_cd/bsddd/Makefile.bsddd
+             Private build wrapper.
+
+     distrib/amd64/ramdisk_cd/bsddd/RAMDISK_CD_DD
+             Private amd64 kernel configuration.
+
+     distrib/amd64/ramdisk_cd/bsddd/list.dd
+             Ramdisk overlay list.
+
+     distrib/amd64/ramdisk_cd/bsddd/dot.profile
+             Single-user profile that replaces itself with /bsddd.
+
+     distrib/amd64/ramdisk_cd/bsddd/bsddd
+             Disk enumeration, parallel overwrite, and status script.
+
+     obj/bsddd.rd
+             Uncompressed ramdisk kernel.
+
+     obj/bsddd.gz
+             Stripped and compressed kernel installed as bsddd.rd.
+
+     obj/minirootXX_dd.img
+             Bootable BIOS/UEFI whole-device image.
+
+CAVEATS
+     This operation is irreversible.  Console confirmation and timeout expiry
+     select every eligible attached disk, including boot media.
+
+     A single logical overwrite pass is not a hardware secure erase.  Remapped
+     sectors, flash translation layers, controller caches, hidden regions, and
+     inaccessible or unsupported devices may retain data.  Use device-native
+     sanitize, secure-erase, or cryptographic-erasure procedures when the
+     required disposal policy covers those areas.
+
+     Audible feedback requires a working PC speaker or console bell.  Verify
+     feedback and storage-controller support before unattended use.
+
+SEE ALSO
+     bioctl(8), dd(1), disklabel(8), make(1), MAKEDEV(8), makefs(8), pcppi(4),
+     pxeboot(8), random(4), rdsetroot(8), speaker(4), sync(8), sysctl(8),
+     vnconfig(8)
+
+BSDDD.RD                       August 18, 2026                       BSDDD.RD
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/bsddd/bsddd
@@ -0,0 +1,365 @@
+#!/bin/ksh
+
+set +o sh
+export PATH=/sbin:/bin:/usr/bin:/usr/sbin:/
+
+POLL_INTERVAL=2
+READY_INTERVAL=10
+PROGRESS_INTERVAL=60
+RESULT_INTERVAL=30
+DD_IF=/dev/zero
+WIPE_TIMEOUT=0
+
+trap '' HUP INT QUIT TSTP
+
+beep() {
+	if [[ -c /dev/speaker ]] &&
+	    print -nr -- 'T240 O4 L8 C' >/dev/speaker 2>/dev/null; then
+		return
+	fi
+	print -n '\a'
+}
+
+beep_pattern() {
+	typeset _count=$1 _interval=$2
+
+	while ((_count > 0)); do
+		beep
+		_count=$((_count - 1))
+		((_count > 0)) && sleep "$_interval"
+	done
+}
+
+result_loop() {
+	typeset _message=$1 _count=$2 _interval=$3
+
+	while :; do
+		echo
+		echo "$_message"
+		beep_pattern "$_count" "$_interval"
+		sleep $RESULT_INTERVAL
+	done
+}
+
+get_disks() {
+	typeset _entry _disk _disks='' _oldifs=$IFS
+
+	IFS=,
+	for _entry in $(sysctl -n hw.disknames); do
+		_disk=${_entry%%:*}
+		case $_disk in
+		sd+([0-9])|wd+([0-9]))
+			_disks="${_disks}${_disks:+ }$_disk"
+			;;
+		esac
+	done
+	IFS=$_oldifs
+	print -r -- "$_disks"
+}
+
+compare_disks() {
+	typeset _disk
+
+	_removed=''
+	_removed_count=0
+	_added_count=0
+	for _disk in $_initial_disks; do
+		case " $_current_disks " in
+		*" $_disk "*) ;;
+		*)
+			_removed="${_removed}${_removed:+ }$_disk"
+			_removed_count=$((_removed_count + 1))
+			;;
+		esac
+	done
+	for _disk in $_current_disks; do
+		case " $_initial_disks " in
+		*" $_disk "*) ;;
+		*)
+			_added_count=$((_added_count + 1))
+			;;
+		esac
+	done
+}
+
+start_reader() {
+	rm -f /tmp/bsddd.answer
+	(
+		typeset _answer
+		IFS= read -r _answer </dev/console || _answer=
+		if [[ $_answer == [yY] ]]; then
+			print -n 'Wipe every attached sd/wd disk, including boot media? [y/N] ' \
+			    >/dev/console
+			IFS= read -r _answer </dev/console || _answer=
+		fi
+		print -r -- "$_answer" > /tmp/bsddd.answer
+	) &
+	_reader=$!
+}
+
+stop_reader() {
+	kill "$_reader" 2>/dev/null || :
+	wait "$_reader" 2>/dev/null || :
+	rm -f /tmp/bsddd.answer
+}
+
+wipe_disk() {
+	typeset _disk=$1 _geometry _sector_size _sectors
+	typeset _sectors_per_mib _full _tail _raw
+
+	(cd /dev && sh MAKEDEV "$_disk") || return 1
+	_raw=/dev/r${_disk}c
+	[[ -c $_raw ]] || return 1
+
+	_geometry=$(disklabel -d "$_disk" 2>/dev/null) || return 1
+	_sector_size=$(print -r -- "$_geometry" |
+	    sed -n 's/^bytes\/sector:[[:space:]]*//p')
+	_sectors=$(print -r -- "$_geometry" |
+	    sed -n 's/^total sectors:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
+
+	case $_sector_size in
+	''|*[!0-9]*) return 1 ;;
+	esac
+	case $_sectors in
+	''|*[!0-9]*) return 1 ;;
+	esac
+	((_sector_size > 0 && _sectors > 0)) || return 1
+
+	echo "$_disk: overwriting $_sectors sectors at $_sector_size bytes"
+
+	if ((1048576 % _sector_size != 0)); then
+		dd if="$DD_IF" of="$_raw" bs="$_sector_size" count="$_sectors" \
+		    status=none || return 1
+	else
+		_sectors_per_mib=$((1048576 / _sector_size))
+		_full=$((_sectors / _sectors_per_mib))
+		_tail=$((_sectors % _sectors_per_mib))
+
+		if ((_full > 0)); then
+			dd if="$DD_IF" of="$_raw" bs=1m count="$_full" \
+			    status=none || return 1
+		fi
+		if ((_tail > 0)); then
+			dd if="$DD_IF" of="$_raw" bs="$_sector_size" \
+			    seek="$((_full * _sectors_per_mib))" count="$_tail" \
+			    status=none || return 1
+		fi
+	fi
+
+	echo "$_disk: complete"
+}
+
+_initial_disks=$(get_disks)
+if [[ -z $_initial_disks ]]; then
+	result_loop 'ERROR: no eligible sd(4) or wd(4) disks were found.' 3 1
+fi
+
+echo
+echo "bsddd.rd is resident in memory."
+echo "Detected disks: $_initial_disks"
+echo "Remove exactly one disk to begin automatically."
+_timeout_deadline=0
+if ((WIPE_TIMEOUT > 0)); then
+	echo "Automatic all-disk wipe in $WIPE_TIMEOUT seconds."
+	_timeout_deadline=$((SECONDS + WIPE_TIMEOUT))
+fi
+echo "Proceed without detected device removal? [y/N]"
+
+start_reader
+_ready_elapsed=$READY_INTERVAL
+_last_disks=$_initial_disks
+_override=no
+
+while :; do
+	_current_disks=$(get_disks)
+	compare_disks
+	if ((_removed_count == 1 && _added_count == 0)); then
+		stop_reader
+		_disks=$_current_disks
+		echo
+		echo "Removed disk: $_removed"
+		break
+	fi
+
+	if [[ $_current_disks != "$_last_disks" ]]; then
+		echo "Waiting; current disks: ${_current_disks:-none}"
+		_last_disks=$_current_disks
+	fi
+
+	if [[ -s /tmp/bsddd.answer ]]; then
+		_answer=$(</tmp/bsddd.answer)
+		wait "$_reader" 2>/dev/null || :
+		rm -f /tmp/bsddd.answer
+		if [[ $_answer == [yY] ]]; then
+			_override=console
+			_disks=$(get_disks)
+			break
+		fi
+		echo "Proceed without detected device removal? [y/N]"
+		start_reader
+	elif ! kill -0 "$_reader" 2>/dev/null; then
+		wait "$_reader" 2>/dev/null || :
+		start_reader
+	fi
+
+	if ((_timeout_deadline > 0 && SECONDS >= _timeout_deadline)); then
+		stop_reader
+		_override=timeout
+		_disks=$(get_disks)
+		break
+	fi
+
+	if ((_ready_elapsed >= READY_INTERVAL)); then
+		if ((_timeout_deadline > 0)); then
+			beep_pattern 4 0.15
+		else
+			beep_pattern 3 0.25
+		fi
+		_ready_elapsed=0
+	fi
+	_sleep=$POLL_INTERVAL
+	if ((_timeout_deadline > 0)); then
+		_remaining=$((_timeout_deadline - SECONDS))
+		((_remaining < _sleep)) && _sleep=$_remaining
+		((_sleep > 0)) || continue
+	fi
+	sleep "$_sleep"
+	_ready_elapsed=$((_ready_elapsed + _sleep))
+done
+
+if ! _softraid_output=$(bioctl softraid0 2>/dev/null); then
+	result_loop 'ERROR: cannot inspect softraid volumes.' 3 1
+fi
+_softraid_volumes=$(print -r -- "$_softraid_output" |
+    sed -n 's/^softraid0.*\(sd[0-9][0-9]*\).*/\1/p')
+for _volume in $_softraid_volumes; do
+	echo "$_volume: detaching softraid volume"
+	if ! bioctl -d "$_volume" >/dev/null 2>&1; then
+		result_loop "ERROR: cannot detach softraid volume: $_volume" 3 1
+	fi
+done
+
+_wipe_disks=
+for _disk in $_disks; do
+	case " $_softraid_volumes " in
+	*" $_disk "*) ;;
+	*) _wipe_disks="${_wipe_disks}${_wipe_disks:+ }$_disk" ;;
+	esac
+done
+_disks=$_wipe_disks
+if [[ -z $_disks ]]; then
+	result_loop 'ERROR: no eligible sd(4) or wd(4) disks remain.' 3 1
+fi
+
+_status_dir=/tmp/bsddd.status
+if ! rm -rf "$_status_dir" || ! mkdir "$_status_dir"; then
+	result_loop 'ERROR: cannot initialize wipe status storage.' 3 1
+fi
+for _disk in $_disks; do
+	_status_file=$_status_dir/$_disk
+	_status_ok=${_status_file}.ok
+	_status_failed=${_status_file}.failed
+	if ! print -r -- running > "$_status_file" ||
+	    ! print -r -- ok > "$_status_ok" ||
+	    ! print -r -- failed > "$_status_failed" ||
+	    [[ $(<"$_status_file") != running ]] ||
+	    [[ $(<"$_status_ok") != ok ]] ||
+	    [[ $(<"$_status_failed") != failed ]]; then
+		result_loop 'ERROR: cannot initialize wipe status storage.' 3 1
+	fi
+done
+
+echo
+if [[ $_override == console ]]; then
+	echo "Console override confirmed."
+elif [[ $_override == timeout ]]; then
+	echo "Wipe timeout expired; all attached sd/wd disks selected."
+fi
+echo "Wiping disks in parallel: $_disks"
+echo "Wipe input: $DD_IF"
+echo "DO NOT POWER OFF."
+beep_pattern 2 1
+
+_jobs=
+_total=0
+for _disk in $_disks; do
+	(
+		_status_file=$_status_dir/$_disk
+		if wipe_disk "$_disk"; then
+			mv "${_status_file}.ok" "$_status_file" || exit 2
+			exit 0
+		else
+			mv "${_status_file}.failed" "$_status_file" || exit 2
+			exit 1
+		fi
+	) &
+	_jobs="${_jobs}${_jobs:+ }$!:$_disk"
+	_total=$((_total + 1))
+done
+
+_elapsed=0
+_status_error=no
+while :; do
+	_done=0
+	for _job in $_jobs; do
+		_pid=${_job%%:*}
+		_disk=${_job#*:}
+		_status_file=$_status_dir/$_disk
+		if [[ ! -r $_status_file ]]; then
+			_status_error=yes
+			break
+		fi
+		_status=$(<"$_status_file")
+		case $_status in
+		ok|failed)
+			_done=$((_done + 1))
+			;;
+		running)
+			if ! kill -0 "$_pid" 2>/dev/null; then
+				_status_error=yes
+				break
+			fi
+			;;
+		*)
+			_status_error=yes
+			break
+			;;
+		esac
+	done
+	[[ $_status_error == yes ]] && break
+	((_done == _total)) && break
+
+	sleep $POLL_INTERVAL
+	_elapsed=$((_elapsed + POLL_INTERVAL))
+	if ((_elapsed >= PROGRESS_INTERVAL)); then
+		echo "Wipe in progress: $_done/$_total disks complete."
+		beep_pattern 1 0
+		_elapsed=0
+	fi
+done
+
+if [[ $_status_error == yes ]]; then
+	echo "ERROR: wipe status unavailable; waiting for disk jobs."
+fi
+for _job in $_jobs; do
+	_pid=${_job%%:*}
+	wait "$_pid" 2>/dev/null || :
+done
+sync
+
+if [[ $_status_error == yes ]]; then
+	result_loop 'ERROR: wipe status unavailable.' 3 1
+fi
+
+_failed=
+for _disk in $_disks; do
+	_status_file=$_status_dir/$_disk
+	[[ $(<"$_status_file") == ok ]] ||
+	    _failed="${_failed}${_failed:+ }$_disk"
+done
+
+if [[ -n $_failed ]]; then
+	result_loop "ERROR: wipe failed: $_failed" 3 1
+fi
+
+result_loop 'WIPE COMPLETE: all selected disks were overwritten.' 5 0.25
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/bsddd/dot.profile
@@ -0,0 +1,16 @@
+set +o sh
+export PATH=/sbin:/bin:/usr/bin:/usr/sbin:/
+
+if ! mount -u /dev/rd0a /; then
+	echo "Cannot remount rd0a read-write."
+	while :; do
+		sleep 3600
+	done
+fi
+
+exec /bsddd
+
+echo "Cannot start /bsddd."
+while :; do
+	sleep 3600
+done
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/bsddd/list.dd
@@ -0,0 +1,7 @@
+#	$OpenBSD$
+
+# bsddd.rd overlay.
+SCRIPT	${CURDIR}/bsddd/dot.profile		.profile
+SCRIPT	${OBJDIR}/bsddd				bsddd
+SPECIAL	chmod 555 bsddd
+SPECIAL	cd dev; sh MAKEDEV speaker

SYNOPSIS

Build on the matching OpenBSD/amd64 release after completing the normal source and release prerequisites. BSDOBJDIR and the amd64 boot files below ${DESTDIR}/usr/mdec must exist. The documented commands require root privileges.

Apply the patch below /usr/src, create the private object link, then build.

Buildraw
set -e

cd /usr/src
patch -p1 < /root/bsddd.rd.patch
cd distrib/amd64/ramdisk_cd/bsddd
make -f Makefile.bsddd obj
make -f Makefile.bsddd

Select pseudorandom input with BSDDD_DD_IF=/dev/urandom.

Build with pseudorandom inputraw
cd /usr/src/distrib/amd64/ramdisk_cd/bsddd
make -f Makefile.bsddd BSDDD_DD_IF=/dev/urandom

Enable a 300-second all-disk timeout with make -f Makefile.bsddd BSDDD_WIPE_TIMEOUT=300.

Install release artifacts when RELEASEDIR is set.

Install release artifactsraw
set -e

cd /usr/src/distrib/amd64/ramdisk_cd/bsddd
make -f Makefile.bsddd install-bsddd \
    RELEASEDIR=/path/to/release

SOURCE LAYOUT

The patch adds one source directory containing six files.

Added filesraw
distrib/amd64/ramdisk_cd/bsddd/
|-- Makefile.bsddd
|-- RAMDISK_CD_DD
|-- README.bsddd
|-- bsddd
|-- dot.profile
`-- list.dd

TARGETS

bsddd
Builds bsddd.rd and minirootXX_dd.img.
install-bsddd
Installs compressed bsddd.rd and the miniroot when RELEASEDIR is set.
unconfig-bsddd
Recovers private mount and vnd(4) state after an interrupted media build. An unowned vnd(4) is not detached.
clean-bsddd, clean, cleandir
Remove private outputs without taking ownership of stock instbin objects.
prepare-unpatch-bsddd
Cleans private outputs, prints the private object path, and removes the source obj symlink.

BUILD MODEL

make obj creates a private object link below BSDOBJDIR. The custom kernel, rdroot, instbin copy, and media objects remain below that path.

The wrapper builds stock ramdisk_cd instbin objects in their normal object directories, then copies instbin into private state. Stock source, configuration, installed use, and cleanup ownership are unchanged. Do not run bsddd concurrently with a build or clean using those objects.

The private rdroot replaces .profile, installs /bsddd, and creates /dev/speaker. It adds no userland binaries. The private kernel retains the stock rdroot size and adds pcppi(4) and speaker(4).

No parent Makefile or SUBDIR list is changed. Normal builds and cleans do not enter the bsddd source directory or its private objects.

Makefile.bsdddrawโ†“
#	$OpenBSD$

.include <bsd.own.mk>

# Stock ramdisk_cd outer-image size, retained as an override point.
BSDDD_FSSIZE?=	11392
BSDDD_DD_IF?=	/dev/zero
BSDDD_WIPE_TIMEOUT?=	0

BSDDD_BASEDIR=	${.CURDIR}/..
BSDDD_TOP=	${.CURDIR}/../../../..
BSDDD_UTILS=	${BSDDD_BASEDIR}/../../miniroot
BSDDD_MTREE=	${BSDDD_UTILS}/mtree.conf
BSDDD_EFIBOOT=	${DESTDIR}/usr/mdec/BOOTX64.EFI \
		${DESTDIR}/usr/mdec/BOOTIA32.EFI
BSDDD_MOUNT_ARGS_MSDOS=	-o-s

BSDDD_RAMDISK=	RAMDISK_CD_DD
BSDDD_KERNEL=	${.OBJDIR}/bsd
BSDDD_FS=	miniroot${OSrev}_dd.img
BSDDD_BUILDOBJDIR=	${.OBJDIR}/build
BSDDD_KERNELOBJDIR=	${BSDDD_BUILDOBJDIR}/kernel/${BSDDD_RAMDISK}
BSDDD_INSTBIN=	${BSDDD_BUILDOBJDIR}/instbin
BSDDD_RDOBJDIR=	${.OBJDIR}/rdobj
BSDDD_MOUNT_POINT=	${.OBJDIR}/mnt
BSDDD_VND=	${.OBJDIR}/vnd
BSDDD_BOOT=	${.OBJDIR}/boot
BSDDD_OBJCHECK=	${.OBJDIR}/.bsddd-obj-ok
BSDDD_MAKEFILE=	${.CURDIR}/Makefile.bsddd
BSDDD_KERNELCONF=	${.CURDIR}/${BSDDD_RAMDISK}
BSDDD_BASECONF=	${BSDDD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD
BSDDD_KERNELCONFDEPS=	${BSDDD_BASECONF}
BSDDD_LISTS=	${BSDDD_BASEDIR}/list ${.CURDIR}/list.dd
BSDDD_SCRIPTSRC=	${.CURDIR}/bsddd
BSDDD_SCRIPT=	${BSDDD_RDOBJDIR}/bsddd
BSDDD_PROFILE=	${.CURDIR}/dot.profile
# Stock 7360-block rdroot; keep paired with RAMDISK_CD_DD MINIROOTSIZE.
BSDDD_RDROOT_SIZE?=	3680k
BSDDD_MRMAKEFSARGS?=	-s ${BSDDD_RDROOT_SIZE} \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096

.PHONY: bsddd
bsddd: bsddd.rd ${BSDDD_FS}

${BSDDD_OBJCHECK}:
	@if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \
		echo "private object directory is not active;" >&2; \
		echo "run 'make -f Makefile.bsddd obj' separately first" >&2; \
		exit 1; \
	fi
	touch $@

${BSDDD_KERNEL}: ${BSDDD_OBJCHECK} ${BSDDD_MAKEFILE} \
	    ${BSDDD_KERNELCONF} ${BSDDD_KERNELCONFDEPS}
	install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${BSDDD_KERNELOBJDIR}
	su ${BUILDUSER} -c \
	    'config -b ${BSDDD_KERNELOBJDIR} -s ${BSDDD_TOP}/sys \
	    ${BSDDD_KERNELCONF} && cd ${BSDDD_KERNELOBJDIR} && \
	    MAKEOBJDIR=${BSDDD_KERNELOBJDIR} ${MAKE} clean && \
	    exec env MAKEOBJDIR=${BSDDD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'
	cp -p ${BSDDD_KERNELOBJDIR}/bsd $@

bsddd.gz: bsddd.rd
	objcopy -g -x -R .comment -R .SUNW_ctf \
	    -K rd_root_size -K rd_root_image \
	    bsddd.rd bsddd.strip
	gzip -9cn bsddd.strip > bsddd.gz

.PHONY: bsddd-stock-deps bsddd-instbin
bsddd-stock-deps: ${BSDDD_OBJCHECK} ${BSDDD_MAKEFILE}
	@_objroot=`cd ${BSDDD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \
	if [ ! -d "$$_objroot" ]; then \
		echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \
		echo "create it before building bsddd" >&2; \
		exit 1; \
	fi
	cd ${BSDDD_TOP}/lib && ${MAKE} obj
	cd ${BSDDD_TOP}/distrib/special && ${MAKE} obj
	cd ${BSDDD_BASEDIR} && ${MAKE} obj
	@_srcdir=`cd ${BSDDD_BASEDIR} && pwd`; \
	_objdir=`cd ${BSDDD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
	if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \
		echo "normal ramdisk_cd object directory is not active" >&2; \
		exit 1; \
	fi
	cd ${BSDDD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}

bsddd-instbin: bsddd-stock-deps
	cd ${BSDDD_BASEDIR} && ${MAKE} ${MFLAGS} instbin
	install -d ${BSDDD_BUILDOBJDIR}
	@_objdir=`cd ${BSDDD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
	if [ ! -f "$$_objdir/instbin" ]; then \
		echo "stock ramdisk_cd instbin was not built" >&2; \
		exit 1; \
	fi; \
	cp -p "$$_objdir/instbin" ${BSDDD_INSTBIN}

bsddd.rd: bsddd-files bsddd-instbin ${BSDDD_KERNEL}
	install -d ${BSDDD_RDOBJDIR}
	rm -f ${BSDDD_RDOBJDIR}/instbin \
	    ${BSDDD_RDOBJDIR}/mr.fs \
	    ${BSDDD_RDOBJDIR}/bsd.rd
	cp -p ${BSDDD_INSTBIN} ${BSDDD_RDOBJDIR}/instbin
	rm -rf ${BSDDD_RDOBJDIR}/mr.fs.d
	install -d -o root -g wheel ${BSDDD_RDOBJDIR}/mr.fs.d
	mtree -def ${BSDDD_MTREE} -p ${BSDDD_RDOBJDIR}/mr.fs.d -u
	CURDIR=${BSDDD_BASEDIR} OBJDIR=${BSDDD_RDOBJDIR} OSrev=${OSrev} \
	    TARGDIR=${BSDDD_RDOBJDIR}/mr.fs.d UTILS=${BSDDD_UTILS} \
	    RELEASEDIR=${RELEASEDIR} sh ${BSDDD_UTILS}/runlist.sh \
	    ${BSDDD_LISTS}
	rm ${BSDDD_RDOBJDIR}/mr.fs.d/instbin
	makefs ${BSDDD_MRMAKEFSARGS} ${BSDDD_RDOBJDIR}/mr.fs \
	    ${BSDDD_RDOBJDIR}/mr.fs.d
	cp -p ${BSDDD_KERNEL} ${BSDDD_RDOBJDIR}/bsd.rd
	rdsetroot ${BSDDD_RDOBJDIR}/bsd.rd ${BSDDD_RDOBJDIR}/mr.fs
	cp ${BSDDD_RDOBJDIR}/bsd.rd $@

${BSDDD_FS}: bsddd.gz
	-umount -f ${BSDDD_MOUNT_POINT} >/dev/null 2>&1
	@if [ -e ${BSDDD_VND} ] && [ ! -s ${BSDDD_VND} ]; then \
		rm -f ${BSDDD_VND}; \
	fi
	@if [ -e ${BSDDD_VND} ]; then \
		echo "stale private vnd state; run" \
		    "'make -f Makefile.bsddd unconfig-bsddd' first" >&2; \
		exit 1; \
	fi
	install -d ${BSDDD_MOUNT_POINT}
	dd if=/dev/zero of=${BSDDD_FS} bs=512 count=${BSDDD_FSSIZE}
	vnconfig -v ${.OBJDIR}/${BSDDD_FS} > ${BSDDD_VND}
	fdisk -yi -l ${BSDDD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
	    `cat ${BSDDD_VND}`
	echo '/ *' | disklabel -wAT- `cat ${BSDDD_VND}`
	newfs -t msdos /dev/r`cat ${BSDDD_VND}`i
	mount ${BSDDD_MOUNT_ARGS_MSDOS} /dev/`cat ${BSDDD_VND}`i \
	    ${BSDDD_MOUNT_POINT}
	mkdir -p ${BSDDD_MOUNT_POINT}/efi/boot
	cp ${BSDDD_EFIBOOT} ${BSDDD_MOUNT_POINT}/efi/boot
	umount ${BSDDD_MOUNT_POINT}
	newfs -O 1 -m 0 -o space -i 524288 -c ${BSDDD_FSSIZE} \
	    /dev/r`cat ${BSDDD_VND}`a
	mount /dev/`cat ${BSDDD_VND}`a ${BSDDD_MOUNT_POINT}
	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${BSDDD_BOOT}
	installboot -v -r ${BSDDD_MOUNT_POINT} `cat ${BSDDD_VND}` \
	    ${DESTDIR}/usr/mdec/biosboot ${BSDDD_BOOT}
	install -c -m 555 -o root -g wheel bsddd.gz \
	    ${BSDDD_MOUNT_POINT}/bsd
	df -i ${BSDDD_MOUNT_POINT}
	umount ${BSDDD_MOUNT_POINT}
	vnconfig -u `cat ${BSDDD_VND}`
	rm -f ${BSDDD_VND}

.PHONY: bsddd-check bsddd-files
bsddd-check:
	@case "${BSDDD_DD_IF}" in \
	/dev/zero|/dev/urandom) ;; \
	*) echo "BSDDD_DD_IF must be /dev/zero or /dev/urandom" >&2; exit 1;; \
	esac
	@case "${BSDDD_WIPE_TIMEOUT}" in \
	''|*[!0-9]*|0[0-9]*) \
		echo "BSDDD_WIPE_TIMEOUT must be 0 or a positive decimal integer" >&2; \
		exit 1;; \
	esac
	@if ! [ "${BSDDD_WIPE_TIMEOUT}" -le 2147483647 ]; then \
		echo "BSDDD_WIPE_TIMEOUT exceeds 2147483647" >&2; \
		exit 1; \
	fi
	test -s ${BSDDD_SCRIPTSRC}
	test -s ${BSDDD_PROFILE}
	test -s ${.CURDIR}/list.dd

bsddd-files: ${BSDDD_OBJCHECK} bsddd-check ${BSDDD_SCRIPTSRC} \
	    ${BSDDD_PROFILE} ${.CURDIR}/list.dd
	install -d ${BSDDD_RDOBJDIR}
	rm -f ${BSDDD_SCRIPT}.tmp
	sed -e 's|^DD_IF=.*|DD_IF=${BSDDD_DD_IF}|' \
	    -e 's|^WIPE_TIMEOUT=.*|WIPE_TIMEOUT=${BSDDD_WIPE_TIMEOUT}|' \
	    ${BSDDD_SCRIPTSRC} > \
	    ${BSDDD_SCRIPT}.tmp
	grep -Fqx 'DD_IF=${BSDDD_DD_IF}' ${BSDDD_SCRIPT}.tmp
	grep -Fqx 'WIPE_TIMEOUT=${BSDDD_WIPE_TIMEOUT}' ${BSDDD_SCRIPT}.tmp
	mv ${BSDDD_SCRIPT}.tmp ${BSDDD_SCRIPT}

.PHONY: unconfig-bsddd
unconfig-bsddd:
	-umount -f ${BSDDD_MOUNT_POINT} >/dev/null 2>&1
	@if [ -e ${BSDDD_VND} ] && [ ! -s ${BSDDD_VND} ]; then \
		rm -f ${BSDDD_VND}; \
	elif [ -f ${BSDDD_VND} ]; then \
		_vnd=`cat ${BSDDD_VND}`; \
		_unit=$${_vnd#vnd}; \
		if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \
			echo "invalid private vnd state: $$_vnd" >&2; exit 1; \
		fi; \
		case "$$_unit" in \
		*[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \
		esac; \
		_info=`vnconfig -l "$$_vnd"` || exit 1; \
		case "$$_info" in \
		"$$_vnd: not in use") rm -f ${BSDDD_VND} ;; \
		"$$_vnd: covering ${.OBJDIR}/${BSDDD_FS} on "*) \
			vnconfig -u "$$_vnd" && rm -f ${BSDDD_VND} ;; \
		*) echo "refusing to detach vnd not owned by bsddd: $$_info" >&2; \
			exit 1 ;; \
		esac; \
	fi

.ifdef RELEASEDIR
.PHONY: install-bsddd
install-bsddd: bsddd.gz ${BSDDD_FS}
	cp bsddd.gz ${RELEASEDIR}/bsddd.rd
	cp ${BSDDD_FS} ${RELEASEDIR}
	chmod a+r ${RELEASEDIR}/bsddd.rd ${RELEASEDIR}/${BSDDD_FS}
.endif

.PHONY: clean-bsddd clean cleandir
clean-bsddd: unconfig-bsddd
	rm -f bsddd.rd bsddd.gz bsddd.strip ${BSDDD_FS} \
	    ${BSDDD_KERNEL} ${BSDDD_BOOT} ${BSDDD_OBJCHECK}
	rm -rf ${BSDDD_BUILDOBJDIR} ${BSDDD_RDOBJDIR}
	-rmdir ${BSDDD_MOUNT_POINT}

clean cleandir: clean-bsddd

.PHONY: prepare-unpatch-bsddd
prepare-unpatch-bsddd: clean-bsddd
	@if [ -L ${.CURDIR}/obj ]; then \
		_obj=`readlink ${.CURDIR}/obj`; \
		echo "empty private object directory may be removed: $$_obj"; \
		rm -f ${.CURDIR}/obj; \
	fi

.include <bsd.obj.mk>
RAMDISK_CD_DDraw
#	$OpenBSD$

include "arch/amd64/conf/RAMDISK_CD"

# Retain an explicit, stock-sized rdroot override for later adjustment.
option		BSDDD_RDROOT
rmoption	MINIROOTSIZE
option		MINIROOTSIZE=7360
rmoption	BSDDD_RDROOT

pcppi0	at isa?
spkr0	at pcppi?
list.ddraw
#	$OpenBSD$

# bsddd.rd overlay.
SCRIPT	${CURDIR}/bsddd/dot.profile		.profile
SCRIPT	${OBJDIR}/bsddd				bsddd
SPECIAL	chmod 555 bsddd
SPECIAL	cd dev; sh MAKEDEV speaker
dot.profileraw
set +o sh
export PATH=/sbin:/bin:/usr/bin:/usr/sbin:/

if ! mount -u /dev/rd0a /; then
	echo "Cannot remount rd0a read-write."
	while :; do
		sleep 3600
	done
fi

exec /bsddd

echo "Cannot start /bsddd."
while :; do
	sleep 3600
done

DISK SELECTION

/bsddd reads hw.disknames with sysctl(8). It selects names matching sd[0-9]* and wd[0-9]*. This includes softraid(4) volumes, their backing disks, and disks presented through common amd64 storage controllers.

Wiping begins when the current set equals the initial set minus exactly one name. Other changes leave the ramdisk waiting. The accepted set is frozen; later arrivals are excluded. The interlock cannot identify the boot medium. Two explicit y responses or timeout expiry select every eligible attached disk. The timer continues during both prompts.

Other device classes are excluded. After the interlock, /bsddd detaches auto-assembled softraid(4) volumes and removes their names from the set. Their backing disks remain selected. Inspection or detach failure stops before writes.

MAKEDEV(8) creates device nodes. disklabel(8) supplies sector size and total sectors. dd(1) writes each whole-disk raw character device in 1 MiB transfers followed by an exact sector-sized tail. Disks run concurrently. One device failure does not stop other jobs. A status failure waits for active jobs. sync(8) precedes the final result.

/bsdddrawโ†“
#!/bin/ksh

set +o sh
export PATH=/sbin:/bin:/usr/bin:/usr/sbin:/

POLL_INTERVAL=2
READY_INTERVAL=10
PROGRESS_INTERVAL=60
RESULT_INTERVAL=30
DD_IF=/dev/zero
WIPE_TIMEOUT=0

trap '' HUP INT QUIT TSTP

beep() {
	if [[ -c /dev/speaker ]] &&
	    print -nr -- 'T240 O4 L8 C' >/dev/speaker 2>/dev/null; then
		return
	fi
	print -n '\a'
}

beep_pattern() {
	typeset _count=$1 _interval=$2

	while ((_count > 0)); do
		beep
		_count=$((_count - 1))
		((_count > 0)) && sleep "$_interval"
	done
}

result_loop() {
	typeset _message=$1 _count=$2 _interval=$3

	while :; do
		echo
		echo "$_message"
		beep_pattern "$_count" "$_interval"
		sleep $RESULT_INTERVAL
	done
}

get_disks() {
	typeset _entry _disk _disks='' _oldifs=$IFS

	IFS=,
	for _entry in $(sysctl -n hw.disknames); do
		_disk=${_entry%%:*}
		case $_disk in
		sd+([0-9])|wd+([0-9]))
			_disks="${_disks}${_disks:+ }$_disk"
			;;
		esac
	done
	IFS=$_oldifs
	print -r -- "$_disks"
}

compare_disks() {
	typeset _disk

	_removed=''
	_removed_count=0
	_added_count=0
	for _disk in $_initial_disks; do
		case " $_current_disks " in
		*" $_disk "*) ;;
		*)
			_removed="${_removed}${_removed:+ }$_disk"
			_removed_count=$((_removed_count + 1))
			;;
		esac
	done
	for _disk in $_current_disks; do
		case " $_initial_disks " in
		*" $_disk "*) ;;
		*)
			_added_count=$((_added_count + 1))
			;;
		esac
	done
}

start_reader() {
	rm -f /tmp/bsddd.answer
	(
		typeset _answer
		IFS= read -r _answer </dev/console || _answer=
		if [[ $_answer == [yY] ]]; then
			print -n 'Wipe every attached sd/wd disk, including boot media? [y/N] ' \
			    >/dev/console
			IFS= read -r _answer </dev/console || _answer=
		fi
		print -r -- "$_answer" > /tmp/bsddd.answer
	) &
	_reader=$!
}

stop_reader() {
	kill "$_reader" 2>/dev/null || :
	wait "$_reader" 2>/dev/null || :
	rm -f /tmp/bsddd.answer
}

wipe_disk() {
	typeset _disk=$1 _geometry _sector_size _sectors
	typeset _sectors_per_mib _full _tail _raw

	(cd /dev && sh MAKEDEV "$_disk") || return 1
	_raw=/dev/r${_disk}c
	[[ -c $_raw ]] || return 1

	_geometry=$(disklabel -d "$_disk" 2>/dev/null) || return 1
	_sector_size=$(print -r -- "$_geometry" |
	    sed -n 's/^bytes\/sector:[[:space:]]*//p')
	_sectors=$(print -r -- "$_geometry" |
	    sed -n 's/^total sectors:[[:space:]]*\([0-9][0-9]*\).*/\1/p')

	case $_sector_size in
	''|*[!0-9]*) return 1 ;;
	esac
	case $_sectors in
	''|*[!0-9]*) return 1 ;;
	esac
	((_sector_size > 0 && _sectors > 0)) || return 1

	echo "$_disk: overwriting $_sectors sectors at $_sector_size bytes"

	if ((1048576 % _sector_size != 0)); then
		dd if="$DD_IF" of="$_raw" bs="$_sector_size" count="$_sectors" \
		    status=none || return 1
	else
		_sectors_per_mib=$((1048576 / _sector_size))
		_full=$((_sectors / _sectors_per_mib))
		_tail=$((_sectors % _sectors_per_mib))

		if ((_full > 0)); then
			dd if="$DD_IF" of="$_raw" bs=1m count="$_full" \
			    status=none || return 1
		fi
		if ((_tail > 0)); then
			dd if="$DD_IF" of="$_raw" bs="$_sector_size" \
			    seek="$((_full * _sectors_per_mib))" count="$_tail" \
			    status=none || return 1
		fi
	fi

	echo "$_disk: complete"
}

_initial_disks=$(get_disks)
if [[ -z $_initial_disks ]]; then
	result_loop 'ERROR: no eligible sd(4) or wd(4) disks were found.' 3 1
fi

echo
echo "bsddd.rd is resident in memory."
echo "Detected disks: $_initial_disks"
echo "Remove exactly one disk to begin automatically."
_timeout_deadline=0
if ((WIPE_TIMEOUT > 0)); then
	echo "Automatic all-disk wipe in $WIPE_TIMEOUT seconds."
	_timeout_deadline=$((SECONDS + WIPE_TIMEOUT))
fi
echo "Proceed without detected device removal? [y/N]"

start_reader
_ready_elapsed=$READY_INTERVAL
_last_disks=$_initial_disks
_override=no

while :; do
	_current_disks=$(get_disks)
	compare_disks
	if ((_removed_count == 1 && _added_count == 0)); then
		stop_reader
		_disks=$_current_disks
		echo
		echo "Removed disk: $_removed"
		break
	fi

	if [[ $_current_disks != "$_last_disks" ]]; then
		echo "Waiting; current disks: ${_current_disks:-none}"
		_last_disks=$_current_disks
	fi

	if [[ -s /tmp/bsddd.answer ]]; then
		_answer=$(</tmp/bsddd.answer)
		wait "$_reader" 2>/dev/null || :
		rm -f /tmp/bsddd.answer
		if [[ $_answer == [yY] ]]; then
			_override=console
			_disks=$(get_disks)
			break
		fi
		echo "Proceed without detected device removal? [y/N]"
		start_reader
	elif ! kill -0 "$_reader" 2>/dev/null; then
		wait "$_reader" 2>/dev/null || :
		start_reader
	fi

	if ((_timeout_deadline > 0 && SECONDS >= _timeout_deadline)); then
		stop_reader
		_override=timeout
		_disks=$(get_disks)
		break
	fi

	if ((_ready_elapsed >= READY_INTERVAL)); then
		if ((_timeout_deadline > 0)); then
			beep_pattern 4 0.15
		else
			beep_pattern 3 0.25
		fi
		_ready_elapsed=0
	fi
	_sleep=$POLL_INTERVAL
	if ((_timeout_deadline > 0)); then
		_remaining=$((_timeout_deadline - SECONDS))
		((_remaining < _sleep)) && _sleep=$_remaining
		((_sleep > 0)) || continue
	fi
	sleep "$_sleep"
	_ready_elapsed=$((_ready_elapsed + _sleep))
done

if ! _softraid_output=$(bioctl softraid0 2>/dev/null); then
	result_loop 'ERROR: cannot inspect softraid volumes.' 3 1
fi
_softraid_volumes=$(print -r -- "$_softraid_output" |
    sed -n 's/^softraid0.*\(sd[0-9][0-9]*\).*/\1/p')
for _volume in $_softraid_volumes; do
	echo "$_volume: detaching softraid volume"
	if ! bioctl -d "$_volume" >/dev/null 2>&1; then
		result_loop "ERROR: cannot detach softraid volume: $_volume" 3 1
	fi
done

_wipe_disks=
for _disk in $_disks; do
	case " $_softraid_volumes " in
	*" $_disk "*) ;;
	*) _wipe_disks="${_wipe_disks}${_wipe_disks:+ }$_disk" ;;
	esac
done
_disks=$_wipe_disks
if [[ -z $_disks ]]; then
	result_loop 'ERROR: no eligible sd(4) or wd(4) disks remain.' 3 1
fi

_status_dir=/tmp/bsddd.status
if ! rm -rf "$_status_dir" || ! mkdir "$_status_dir"; then
	result_loop 'ERROR: cannot initialize wipe status storage.' 3 1
fi
for _disk in $_disks; do
	_status_file=$_status_dir/$_disk
	_status_ok=${_status_file}.ok
	_status_failed=${_status_file}.failed
	if ! print -r -- running > "$_status_file" ||
	    ! print -r -- ok > "$_status_ok" ||
	    ! print -r -- failed > "$_status_failed" ||
	    [[ $(<"$_status_file") != running ]] ||
	    [[ $(<"$_status_ok") != ok ]] ||
	    [[ $(<"$_status_failed") != failed ]]; then
		result_loop 'ERROR: cannot initialize wipe status storage.' 3 1
	fi
done

echo
if [[ $_override == console ]]; then
	echo "Console override confirmed."
elif [[ $_override == timeout ]]; then
	echo "Wipe timeout expired; all attached sd/wd disks selected."
fi
echo "Wiping disks in parallel: $_disks"
echo "Wipe input: $DD_IF"
echo "DO NOT POWER OFF."
beep_pattern 2 1

_jobs=
_total=0
for _disk in $_disks; do
	(
		_status_file=$_status_dir/$_disk
		if wipe_disk "$_disk"; then
			mv "${_status_file}.ok" "$_status_file" || exit 2
			exit 0
		else
			mv "${_status_file}.failed" "$_status_file" || exit 2
			exit 1
		fi
	) &
	_jobs="${_jobs}${_jobs:+ }$!:$_disk"
	_total=$((_total + 1))
done

_elapsed=0
_status_error=no
while :; do
	_done=0
	for _job in $_jobs; do
		_pid=${_job%%:*}
		_disk=${_job#*:}
		_status_file=$_status_dir/$_disk
		if [[ ! -r $_status_file ]]; then
			_status_error=yes
			break
		fi
		_status=$(<"$_status_file")
		case $_status in
		ok|failed)
			_done=$((_done + 1))
			;;
		running)
			if ! kill -0 "$_pid" 2>/dev/null; then
				_status_error=yes
				break
			fi
			;;
		*)
			_status_error=yes
			break
			;;
		esac
	done
	[[ $_status_error == yes ]] && break
	((_done == _total)) && break

	sleep $POLL_INTERVAL
	_elapsed=$((_elapsed + POLL_INTERVAL))
	if ((_elapsed >= PROGRESS_INTERVAL)); then
		echo "Wipe in progress: $_done/$_total disks complete."
		beep_pattern 1 0
		_elapsed=0
	fi
done

if [[ $_status_error == yes ]]; then
	echo "ERROR: wipe status unavailable; waiting for disk jobs."
fi
for _job in $_jobs; do
	_pid=${_job%%:*}
	wait "$_pid" 2>/dev/null || :
done
sync

if [[ $_status_error == yes ]]; then
	result_loop 'ERROR: wipe status unavailable.' 3 1
fi

_failed=
for _disk in $_disks; do
	_status_file=$_status_dir/$_disk
	[[ $(<"$_status_file") == ok ]] ||
	    _failed="${_failed}${_failed:+ }$_disk"
done

if [[ -n $_failed ]]; then
	result_loop "ERROR: wipe failed: $_failed" 3 1
fi

result_loop 'WIPE COMPLETE: all selected disks were overwritten.' 5 0.25

FEEDBACK

Three short bells
Waiting without a timeout.
Four rapid bells
Waiting with an all-disk timeout.
Two separated bells
Disk writes started.
One bell
Wipe progress.
Five short bells
All selected disks completed.
Three separated bells
Setup, status, or disk-write failure.

Ready patterns repeat every 10 seconds. Completion and failure patterns repeat every 30 seconds. Progress is reported every 60 seconds. A console bell is used if speaker output fails. The result loop continues until power-off or reset.

USE

As root, write the miniroot to removable media. This replaces the selected device's partition table and filesystems. Verify the output device.

Write media from OpenBSDraw
set -e

dd if=./miniroot79_dd.img of=/dev/rsdXc bs=1m
sync

Boot the target. At the ready pattern, remove the boot medium. Removal of any eligible disk satisfies the name-based interlock. Two separated bells mark write startup. Wait for the repeating completion pattern before power-off. Stock amd64 BIOS and UEFI boot programs are used. Secure Boot is unsupported.

A boot loader or matching OpenBSD/amd64 pxeboot(8) may load the compressed bsddd.rd directly. Without removable media, use the console confirmations.

REBUILD AND CLEAN

Clean and rebuild private outputs.

Rebuildraw
set -e

cd /usr/src/distrib/amd64/ramdisk_cd/bsddd
make -f Makefile.bsddd clean
make -f Makefile.bsddd

Run unconfig-bsddd first after an interrupted media build. Prepare private state before reversal.

Clean and reverse patchraw
set -e

cd /usr/src/distrib/amd64/ramdisk_cd/bsddd
make -f Makefile.bsddd prepare-unpatch-bsddd
cd /usr/src
patch -R -p1 < /root/bsddd.rd.patch
rmdir distrib/amd64/ramdisk_cd/bsddd

VARIABLES

BSDDD_DD_IF
Build-time dd(1) input device. Default: /dev/zero. /dev/urandom supplies non-blocking pseudorandom data but may limit throughput. No other value is accepted.
BSDDD_WIPE_TIMEOUT
Build-time automatic all-disk deadline in seconds. Default: 0 (disabled). Values from 1 through 2147483647 enable it. Expiry selects every eligible attached disk, including boot media.
BSDDD_FSSIZE
Outer media size in 512-byte blocks. Default: 11392, matching stock ramdisk_cd.
BSDDD_RDROOT_SIZE
Private rdroot size. Default: 3680k, matching 7360 512-byte blocks. Overrides must match MINIROOTSIZE.
BSDDD_MRMAKEFSARGS
makefs(8) arguments for the private rdroot. The default uses BSDDD_RDROOT_SIZE and stock filesystem parameters.
BSDOBJDIR
Normal object root and base for the private object link. Default: /usr/obj.
DESTDIR
Optional prefix for amd64 boot files under usr/mdec. Default: empty.
RELEASEDIR
Enables install-bsddd and names its destination.

FILES

obj/bsddd.rd
Uncompressed ramdisk kernel.
obj/bsddd.gz
Compressed kernel installed as bsddd.rd.
obj/minirootXX_dd.img
Bootable BIOS/UEFI whole-device image.

CAVEATS

This operation is irreversible. Console confirmation and timeout expiry select every eligible attached disk, including boot media.

A single logical overwrite pass is not a hardware secure erase. Remapped sectors, flash translation layers, controller caches, hidden regions, and inaccessible or unsupported devices may retain data. Use device-native sanitize, secure-erase, or cryptographic-erasure procedures when the required disposal policy covers those areas.

Audible feedback requires a working PC speaker or console bell. Verify feedback and storage-controller support before unattended use.

SEE ALSO

bioctl(8), dd(1), disklabel(8), make(1), MAKEDEV(8), makefs(8), pcppi(4), pxeboot(8), random(4), rdsetroot(8), speaker(4), sync(8), sysctl(8), vnconfig(8)

Patch Notes

bsdsshd.rd

OpenBSD/amd64 installer ramdisk and miniroot targets with sshd access.

Downloadbsdsshd.rd.patchApplies below /usr/src46 KiBSHA256 (bsdsshd.rd.patch) = 155372ff85ab2b04a2be06bec0b2c86a9cab1397d400c27b5e51e697d1c828f1

DESCRIPTION

The patch adds separate bsdsshd.rd and minirootXX_sshd.img targets. The ramdisk configures networking from /auto_install.conf, starts sshd, and leaves the installer environment available to the remote root user.

The default retains the local installer menu and shell. RDSSHD_CONSOLE_LOCK=yes removes the local installer userland session. Boot-loader and kernel console output remain visible.

The target supports remote storage preparation and installation, including softraid(4) work with bioctl(8). Use autoinstall(8), install.site(5), and siteXX.tgz for fully unattended installation.

The patch adds files only. Stock source files, Makefiles, bsd.rd, minirootXX.img, and cdXX.iso targets are unchanged. Enhanced targets are available only through Makefile.rdsshd.

PATCH

The aggregate patch contains the build wrapper, kernel configurations, overlay patches, ramdisk files, and plain-text reference.

bsdsshd.rd.patchrawโ†“
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/Makefile.rdsshd
@@ -0,0 +1,411 @@
+#	$OpenBSD$
+
+.include <bsd.own.mk>
+
+RDSSHD_AUTHORIZED_KEYS?=
+RDSSHD_HOST_KEY?=
+RDSSHD_CONSOLE_LOCK?=	no
+RDSSHD_KERNEL_STACK_PROTECTOR?=	yes
+RDSSHD_PORT?=	22
+RDSSHD_AI_IF?=	em0
+RDSSHD_AI_HOSTNAME?=	rdinstall
+RDSSHD_AI_IPV4?=	autoconf
+RDSSHD_AI_NETMASK?=	255.255.255.0
+RDSSHD_AI_ROUTE?=	none
+RDSSHD_AI_IPV6?=	none
+RDSSHD_AI_DOMAIN?=	my.domain
+RDSSHD_AI_DNS?=	none
+RDSSHD_FSSIZE?=	32768
+
+RDSSHD_BASEDIR=	${.CURDIR}/..
+RDSSHD_TOP=	${.CURDIR}/../../../..
+RDSSHD_UTILS=	${RDSSHD_BASEDIR}/../../miniroot
+RDSSHD_MTREE=	${RDSSHD_UTILS}/mtree.conf
+RDSSHD_EFIBOOT=	${DESTDIR}/usr/mdec/BOOTX64.EFI \
+		${DESTDIR}/usr/mdec/BOOTIA32.EFI
+RDSSHD_MOUNT_ARGS_MSDOS=	-o-s
+
+RDSSHD_RAMDISK=	RAMDISK_CD_SSHD
+RDSSHD_KERNEL=	${.OBJDIR}/bsd
+.if ${RDSSHD_KERNEL_STACK_PROTECTOR:L} == "no"
+RDSSHD_RAMDISK=	RAMDISK_CD_SSHD_NO_PROPOLICE
+RDSSHD_KERNEL=	${.OBJDIR}/bsd.no-propolice
+.endif
+RDSSHD_FS=	miniroot${OSrev}_sshd.img
+RDSSHD_BUILDOBJDIR=	${.OBJDIR}/build
+RDSSHD_KERNELOBJDIR=	${RDSSHD_BUILDOBJDIR}/kernel/${RDSSHD_RAMDISK}
+RDSSHD_INSTBIN=	${RDSSHD_BUILDOBJDIR}/instbin
+RDSSHD_STAGE=	${.OBJDIR}/stage
+RDSSHD_RDOBJDIR=	${.OBJDIR}/rdobj
+RDSSHD_SSHOBJDIR=	${.OBJDIR}/sshobj
+RDSSHD_INITOBJDIR=	${.OBJDIR}/initobj
+RDSSHD_INITSRCDIR=	${.OBJDIR}/initsrc
+RDSSHD_MOUNT_POINT=	${.OBJDIR}/mnt
+RDSSHD_VND=	${.OBJDIR}/vnd
+RDSSHD_BOOT=	${.OBJDIR}/boot
+RDSSHD_OBJCHECK=	${.OBJDIR}/.rdsshd-obj-ok
+RDSSHD_MAKEFILE=	${.CURDIR}/Makefile.rdsshd
+RDSSHD_INITMAKEFILE=	${.CURDIR}/Makefile.init
+RDSSHD_KERNELCONF=	${.CURDIR}/${RDSSHD_RAMDISK}
+RDSSHD_BASECONF=	${RDSSHD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD
+RDSSHD_KERNELCONFDEPS=	${RDSSHD_BASECONF}
+RDSSHD_LISTS=	${RDSSHD_BASEDIR}/list ${.CURDIR}/list.sshd \
+		${RDSSHD_STAGE}/list.console
+RDSSHD_INITSRC=	${RDSSHD_INITSRCDIR}/init.c
+RDSSHD_INIT=	${RDSSHD_STAGE}/init
+RDSSHD_INSTALLERPATCH=	${.CURDIR}/installer.sshd.patch
+RDSSHD_CONSOLEPATCH=	${.CURDIR}/console-lock.sshd.patch
+RDSSHD_INITPATCH=	${.CURDIR}/init.sshd.patch
+RDSSHD_SSHDCONF=	${.CURDIR}/sshd_config
+RDSSHD_MRMAKEFSARGS?=	-s 20m \
+		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
+RDSSHD_BINS=	${RDSSHD_STAGE}/bin/sshd \
+		${RDSSHD_STAGE}/bin/ssh-keygen \
+		${RDSSHD_STAGE}/bin/sshd-session \
+		${RDSSHD_STAGE}/bin/sshd-auth
+
+RDSSHD_INITDEP=
+.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
+RDSSHD_INITDEP=	${RDSSHD_INIT}
+.endif
+
+.PHONY: rdsshd
+rdsshd: bsdsshd.rd ${RDSSHD_FS}
+
+.PHONY: rdsshd-objcheck
+rdsshd-objcheck: ${RDSSHD_OBJCHECK}
+
+${RDSSHD_OBJCHECK}:
+	@if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \
+		echo "private object directory is not active;" >&2; \
+		echo "run 'make -f Makefile.rdsshd obj' separately first" >&2; \
+		exit 1; \
+	fi
+	touch $@
+
+${RDSSHD_KERNEL}: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE} \
+	    ${RDSSHD_KERNELCONF} ${RDSSHD_KERNELCONFDEPS}
+	install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${RDSSHD_KERNELOBJDIR}
+	su ${BUILDUSER} -c \
+	    'config -b ${RDSSHD_KERNELOBJDIR} -s ${RDSSHD_TOP}/sys \
+	    ${RDSSHD_KERNELCONF} && cd ${RDSSHD_KERNELOBJDIR} && \
+	    MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} clean && \
+	    exec env MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'
+	cp -p ${RDSSHD_KERNELOBJDIR}/bsd $@
+
+bsdsshd.gz: bsdsshd.rd
+	objcopy -g -x -R .comment -R .SUNW_ctf \
+	    -K rd_root_size -K rd_root_image \
+	    bsdsshd.rd bsdsshd.strip
+	gzip -9cn bsdsshd.strip > bsdsshd.gz
+.if !empty(RDSSHD_HOST_KEY)
+	chmod 600 bsdsshd.strip $@
+.endif
+
+.PHONY: rdsshd-stock-deps rdsshd-instbin
+rdsshd-stock-deps: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	@_objroot=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \
+	if [ ! -d "$$_objroot" ]; then \
+		echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \
+		echo "create it before building rdsshd" >&2; \
+		exit 1; \
+	fi
+	cd ${RDSSHD_TOP}/lib && ${MAKE} obj
+	cd ${RDSSHD_TOP}/distrib/special && ${MAKE} obj
+	cd ${RDSSHD_BASEDIR} && ${MAKE} obj
+	@_srcdir=`cd ${RDSSHD_BASEDIR} && pwd`; \
+	_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
+	if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \
+		echo "normal ramdisk_cd object directory is not active" >&2; \
+		exit 1; \
+	fi
+	cd ${RDSSHD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}
+
+rdsshd-instbin: rdsshd-stock-deps
+	cd ${RDSSHD_BASEDIR} && ${MAKE} ${MFLAGS} instbin
+	install -d ${RDSSHD_BUILDOBJDIR}
+	@_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
+	if [ ! -f "$$_objdir/instbin" ]; then \
+		echo "stock ramdisk_cd instbin was not built" >&2; \
+		exit 1; \
+	fi; \
+	cp -p "$$_objdir/instbin" ${RDSSHD_INSTBIN}
+
+bsdsshd.rd: rdsshd-files ${RDSSHD_INITDEP} rdsshd-instbin \
+	    ${RDSSHD_KERNEL}
+	install -d ${RDSSHD_RDOBJDIR}
+	rm -f ${RDSSHD_RDOBJDIR}/instbin \
+	    ${RDSSHD_RDOBJDIR}/mr.fs \
+	    ${RDSSHD_RDOBJDIR}/bsd.rd
+	cp -p ${RDSSHD_INSTBIN} ${RDSSHD_RDOBJDIR}/instbin
+	rm -rf ${RDSSHD_RDOBJDIR}/mr.fs.d
+	install -d -o root -g wheel ${RDSSHD_RDOBJDIR}/mr.fs.d
+	mtree -def ${RDSSHD_MTREE} -p ${RDSSHD_RDOBJDIR}/mr.fs.d -u
+	CURDIR=${RDSSHD_BASEDIR} OBJDIR=${RDSSHD_RDOBJDIR} OSrev=${OSrev} \
+	    TARGDIR=${RDSSHD_RDOBJDIR}/mr.fs.d UTILS=${RDSSHD_UTILS} \
+	    RELEASEDIR=${RELEASEDIR} sh ${RDSSHD_UTILS}/runlist.sh \
+	    ${RDSSHD_LISTS}
+	rm ${RDSSHD_RDOBJDIR}/mr.fs.d/instbin
+	makefs ${RDSSHD_MRMAKEFSARGS} ${RDSSHD_RDOBJDIR}/mr.fs \
+	    ${RDSSHD_RDOBJDIR}/mr.fs.d
+	cp -p ${RDSSHD_KERNEL} ${RDSSHD_RDOBJDIR}/bsd.rd
+	rdsetroot ${RDSSHD_RDOBJDIR}/bsd.rd ${RDSSHD_RDOBJDIR}/mr.fs
+	cp ${RDSSHD_RDOBJDIR}/bsd.rd $@
+.if !empty(RDSSHD_HOST_KEY)
+	chmod 600 ${RDSSHD_RDOBJDIR}/mr.fs \
+	    ${RDSSHD_RDOBJDIR}/bsd.rd $@
+.endif
+
+${RDSSHD_FS}: bsdsshd.gz
+	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
+	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
+		rm -f ${RDSSHD_VND}; \
+	fi
+	@if [ -e ${RDSSHD_VND} ]; then \
+		echo "stale private vnd state; run" \
+		    "'make -f Makefile.rdsshd unconfig-rdsshd' first" >&2; \
+		exit 1; \
+	fi
+	install -d ${RDSSHD_MOUNT_POINT}
+	dd if=/dev/zero of=${RDSSHD_FS} bs=512 count=${RDSSHD_FSSIZE}
+	vnconfig -v ${.OBJDIR}/${RDSSHD_FS} > ${RDSSHD_VND}
+	fdisk -yi -l ${RDSSHD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
+	    `cat ${RDSSHD_VND}`
+	echo '/ *' | disklabel -wAT- `cat ${RDSSHD_VND}`
+	newfs -t msdos /dev/r`cat ${RDSSHD_VND}`i
+	mount ${RDSSHD_MOUNT_ARGS_MSDOS} /dev/`cat ${RDSSHD_VND}`i \
+	    ${RDSSHD_MOUNT_POINT}
+	mkdir -p ${RDSSHD_MOUNT_POINT}/efi/boot
+	cp ${RDSSHD_EFIBOOT} ${RDSSHD_MOUNT_POINT}/efi/boot
+	umount ${RDSSHD_MOUNT_POINT}
+	newfs -O 1 -m 0 -o space -i 524288 -c ${RDSSHD_FSSIZE} \
+	    /dev/r`cat ${RDSSHD_VND}`a
+	mount /dev/`cat ${RDSSHD_VND}`a ${RDSSHD_MOUNT_POINT}
+	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${RDSSHD_BOOT}
+	installboot -v -r ${RDSSHD_MOUNT_POINT} `cat ${RDSSHD_VND}` \
+	    ${DESTDIR}/usr/mdec/biosboot ${RDSSHD_BOOT}
+	install -c -m 555 -o root -g wheel bsdsshd.gz \
+	    ${RDSSHD_MOUNT_POINT}/bsd
+	df -i ${RDSSHD_MOUNT_POINT}
+	umount ${RDSSHD_MOUNT_POINT}
+	vnconfig -u `cat ${RDSSHD_VND}`
+	rm -f ${RDSSHD_VND}
+.if !empty(RDSSHD_HOST_KEY)
+	chmod 600 $@
+.endif
+
+.PHONY: rdsshd-check rdsshd-files rdsshd-scripts
+rdsshd-check:
+	@if [ -z "${RDSSHD_AUTHORIZED_KEYS}" ]; then \
+		echo "set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2; \
+		exit 1; \
+	fi
+	@case "${RDSSHD_CONSOLE_LOCK:L}" in \
+	yes|no) ;; \
+	*) echo "RDSSHD_CONSOLE_LOCK must be yes or no" >&2; exit 1;; \
+	esac
+	@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}" in \
+	yes|no) ;; \
+	*) echo "RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2; exit 1;; \
+	esac
+
+${RDSSHD_INITSRC}: ${RDSSHD_OBJCHECK} ${RDSSHD_TOP}/sbin/init/init.c \
+	    ${RDSSHD_INITPATCH}
+	install -d ${RDSSHD_INITSRCDIR}
+	rm -f ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}.tmp.orig \
+	    ${RDSSHD_INITSRC}.tmp.rej
+	cp ${RDSSHD_TOP}/sbin/init/init.c ${RDSSHD_INITSRC}.tmp
+	cd ${RDSSHD_INITSRCDIR} && \
+	    patch -f -s -p0 -F0 < ${RDSSHD_INITPATCH}
+	mv ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}
+
+${RDSSHD_INIT}: ${RDSSHD_INITSRC} \
+	    ${RDSSHD_TOP}/sbin/init/pathnames.h ${RDSSHD_INITMAKEFILE} \
+	    ${RDSSHD_BASEDIR}/../../special/init/Makefile \
+	    ${RDSSHD_BASEDIR}/../../special/Makefile.inc
+	install -d ${RDSSHD_INITOBJDIR} ${RDSSHD_STAGE}
+	cd ${RDSSHD_BASEDIR}/../../special/init && \
+	    MAKEOBJDIR=${RDSSHD_INITOBJDIR} ${MAKE} ${MFLAGS} \
+	    -f ${RDSSHD_INITMAKEFILE} \
+	    RDSSHD_INITSRCDIR=${RDSSHD_INITSRCDIR} init
+	install -c -s ${RDSSHD_INITOBJDIR}/init $@
+
+rdsshd-scripts: ${RDSSHD_OBJCHECK} ${RDSSHD_UTILS}/install.sub \
+	    ${RDSSHD_UTILS}/dot.profile \
+	    ${RDSSHD_INSTALLERPATCH} ${RDSSHD_CONSOLEPATCH}
+	install -d ${RDSSHD_STAGE}
+	rm -f ${RDSSHD_STAGE}/install.sub.tmp \
+	    ${RDSSHD_STAGE}/install.sub.tmp.orig \
+	    ${RDSSHD_STAGE}/install.sub.tmp.rej \
+	    ${RDSSHD_STAGE}/dot.profile.tmp \
+	    ${RDSSHD_STAGE}/dot.profile.tmp.orig \
+	    ${RDSSHD_STAGE}/dot.profile.tmp.rej
+	cp ${RDSSHD_UTILS}/install.sub ${RDSSHD_STAGE}/install.sub.tmp
+	cp ${RDSSHD_UTILS}/dot.profile ${RDSSHD_STAGE}/dot.profile.tmp
+	cd ${RDSSHD_STAGE} && \
+	    patch -f -s -p0 -F0 < ${RDSSHD_INSTALLERPATCH}
+.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
+	cd ${RDSSHD_STAGE} && \
+	    patch -f -s -p0 -F0 < ${RDSSHD_CONSOLEPATCH}
+.endif
+	chmod 755 ${RDSSHD_STAGE}/install.sub.tmp
+	mv ${RDSSHD_STAGE}/install.sub.tmp ${RDSSHD_STAGE}/install.sub
+	mv ${RDSSHD_STAGE}/dot.profile.tmp ${RDSSHD_STAGE}/dot.profile
+
+rdsshd-files: rdsshd-check rdsshd-scripts ${RDSSHD_BINS} \
+	    ${RDSSHD_SSHDCONF}
+	install -d ${RDSSHD_STAGE}
+	@if [ "${RDSSHD_CONSOLE_LOCK:L}" = yes ]; then \
+		echo 'COPY ${RDSSHD_INIT} sbin/init'; \
+	fi > ${RDSSHD_STAGE}/list.console
+	sed '/^root:/s|:/bin/ksh$$|:/bin/sh|' \
+	    ${RDSSHD_UTILS}/master.passwd > ${RDSSHD_STAGE}/master.passwd
+	grep '^root:.*:/bin/sh$$' ${RDSSHD_STAGE}/master.passwd >/dev/null
+	sed -e '/^[	 ]*$$/d' -e '/^[	 ]*#/d' \
+	    < "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
+	test -s ${RDSSHD_STAGE}/authorized_keys
+	@_n=0; while IFS= read -r _key; do \
+		_n=$$((_n + 1)); \
+		if ! printf '%s\n' "$$_key" | \
+		    ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \
+			echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \
+			exit 1; \
+		fi; \
+	done < ${RDSSHD_STAGE}/authorized_keys
+	grep '^sshd:' ${RDSSHD_STAGE}/master.passwd >/dev/null || \
+	    grep '^sshd:' ${RDSSHD_TOP}/etc/master.passwd >> \
+	    ${RDSSHD_STAGE}/master.passwd
+	cp ${RDSSHD_UTILS}/group ${RDSSHD_STAGE}/group
+	grep '^sshd:' ${RDSSHD_STAGE}/group >/dev/null || \
+	    grep '^sshd:' ${RDSSHD_TOP}/etc/group >> ${RDSSHD_STAGE}/group
+	{ \
+	    printf 'System hostname = %s\n' '${RDSSHD_AI_HOSTNAME}'; \
+	    printf 'Network interface to configure = %s\n' '${RDSSHD_AI_IF}'; \
+	    printf 'IPv4 address for %s = %s\n' \
+		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV4}'; \
+	    case '${RDSSHD_AI_IPV4}' in \
+	    none|autoconf|dhcp) ;; \
+	    *) printf 'Netmask for %s = %s\n' \
+		'${RDSSHD_AI_IF}' '${RDSSHD_AI_NETMASK}'; \
+	       printf 'Default IPv4 route = %s\n' '${RDSSHD_AI_ROUTE}' ;; \
+	    esac; \
+	    printf 'IPv6 address for %s = %s\n' \
+		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV6}'; \
+	    printf 'Network interface to configure = done\n'; \
+	    printf 'DNS domain name = %s\n' '${RDSSHD_AI_DOMAIN}'; \
+	    printf 'DNS nameservers = %s\n' '${RDSSHD_AI_DNS}'; \
+	} > ${RDSSHD_STAGE}/auto_install.conf
+	sed 's|^Port .*|Port ${RDSSHD_PORT}|' ${RDSSHD_SSHDCONF} > \
+	    ${RDSSHD_STAGE}/sshd_config
+	rm -f ${RDSSHD_STAGE}/ssh_host_ed25519_key \
+	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.pub \
+	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test \
+	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test.pub \
+	    ${RDSSHD_STAGE}/sshd_config.test
+	@if [ -n "${RDSSHD_HOST_KEY}" ]; then \
+		if ! ${RDSSHD_STAGE}/bin/ssh-keygen -y -P '' \
+		    -f "${RDSSHD_HOST_KEY}" 2>/dev/null | grep -q '^ssh-ed25519 '; then \
+			echo "RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key" >&2; \
+			exit 1; \
+		fi; \
+		install -c -m 600 "${RDSSHD_HOST_KEY}" \
+		    ${RDSSHD_STAGE}/ssh_host_ed25519_key; \
+	fi
+	@_key=${RDSSHD_STAGE}/ssh_host_ed25519_key; \
+	_testkey=${RDSSHD_STAGE}/ssh_host_ed25519_key.test; \
+	_testconf=${RDSSHD_STAGE}/sshd_config.test; \
+	if [[ ! -s $$_key ]]; then \
+		_key=$$_testkey; \
+		${RDSSHD_STAGE}/bin/ssh-keygen -q -t ed25519 -N '' \
+		    -f $$_key || exit 1; \
+	fi; \
+	sed "s|^HostKey .*|HostKey $$_key|" \
+	    ${RDSSHD_STAGE}/sshd_config > $$_testconf; \
+	${RDSSHD_STAGE}/bin/sshd -t -f $$_testconf; \
+	_status=$$?; \
+	rm -f $$_testconf $$_testkey $$_testkey.pub; \
+	exit $$_status
+${RDSSHD_STAGE}/bin/sshd: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd
+	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/sshd/sshd $@
+
+${RDSSHD_STAGE}/bin/ssh-keygen: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/ssh-keygen
+	cd ${RDSSHD_TOP}/usr.bin/ssh/ssh-keygen && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/ssh-keygen ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/ssh-keygen/ssh-keygen $@
+
+${RDSSHD_STAGE}/bin/sshd-session: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-session
+	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-session && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-session ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-session/sshd-session $@
+
+${RDSSHD_STAGE}/bin/sshd-auth: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-auth
+	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-auth && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-auth ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-auth/sshd-auth $@
+
+.PHONY: unconfig-rdsshd
+unconfig-rdsshd:
+	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
+	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
+		rm -f ${RDSSHD_VND}; \
+	elif [ -f ${RDSSHD_VND} ]; then \
+		_vnd=`cat ${RDSSHD_VND}`; \
+		_unit=$${_vnd#vnd}; \
+		if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \
+			echo "invalid private vnd state: $$_vnd" >&2; exit 1; \
+		fi; \
+		case "$$_unit" in \
+		*[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \
+		esac; \
+		_info=`vnconfig -l "$$_vnd"` || exit 1; \
+		case "$$_info" in \
+		"$$_vnd: not in use") rm -f ${RDSSHD_VND} ;; \
+		"$$_vnd: covering ${.OBJDIR}/${RDSSHD_FS} on "*) \
+			vnconfig -u "$$_vnd" && rm -f ${RDSSHD_VND} ;; \
+		*) echo "refusing to detach vnd not owned by rdsshd: $$_info" >&2; \
+			exit 1 ;; \
+		esac; \
+	fi
+
+.ifdef RELEASEDIR
+.PHONY: install-rdsshd
+install-rdsshd: bsdsshd.gz ${RDSSHD_FS}
+	cp bsdsshd.gz ${RELEASEDIR}/bsdsshd.rd
+	cp ${RDSSHD_FS} ${RELEASEDIR}
+.if empty(RDSSHD_HOST_KEY)
+	chmod a+r ${RELEASEDIR}/bsdsshd.rd
+.else
+	chmod 600 ${RELEASEDIR}/bsdsshd.rd ${RELEASEDIR}/${RDSSHD_FS}
+.endif
+.endif
+
+.PHONY: clean-rdsshd clean cleandir
+clean-rdsshd: unconfig-rdsshd
+	rm -f bsdsshd.rd bsdsshd.gz bsdsshd.strip ${RDSSHD_FS} \
+	    ${.OBJDIR}/bsd ${.OBJDIR}/bsd.no-propolice ${RDSSHD_BOOT} \
+	    ${RDSSHD_OBJCHECK}
+	rm -rf ${RDSSHD_BUILDOBJDIR} ${RDSSHD_STAGE} ${RDSSHD_RDOBJDIR} \
+	    ${RDSSHD_SSHOBJDIR} ${RDSSHD_INITOBJDIR} ${RDSSHD_INITSRCDIR}
+	-rmdir ${RDSSHD_MOUNT_POINT}
+
+clean cleandir: clean-rdsshd
+
+.PHONY: prepare-unpatch-rdsshd
+prepare-unpatch-rdsshd: clean-rdsshd
+	@if [ -L ${.CURDIR}/obj ]; then \
+		_obj=`readlink ${.CURDIR}/obj`; \
+		echo "empty private object directory may be removed: $$_obj"; \
+		rm -f ${.CURDIR}/obj; \
+	fi
+
+.include <bsd.obj.mk>
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/Makefile.init
@@ -0,0 +1,5 @@
+#	$OpenBSD$
+
+.PATH: ${RDSSHD_INITSRCDIR}
+CPPFLAGS+=	-I${.CURDIR}/../../../sbin/init
+.include "${.CURDIR}/Makefile"
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/README.rdsshd
@@ -0,0 +1,599 @@
+BSDSSHD.RD                 bsdsshd.rd build notes                 BSDSSHD.RD
+
+NAME
+     bsdsshd.rd - OpenBSD/amd64 installer ramdisk with sshd access
+
+DESCRIPTION
+     The patch adds separate bsdsshd.rd and minirootXX_sshd.img targets.
+     The ramdisk configures networking from /auto_install.conf, starts sshd,
+     and leaves the installer environment available to the remote root user.
+
+     The default retains the local installer menu and shell.
+     RDSSHD_CONSOLE_LOCK=yes removes the local installer userland session.
+     Boot-loader and kernel console output remain visible.
+
+     The target supports remote storage preparation and installation, including
+     softraid(4) work with bioctl(8).  Use autoinstall(8), install.site(5), and
+     siteXX.tgz when full unattended installation is appropriate.
+
+     The patch adds files only.  Stock source files, Makefiles, bsd.rd,
+     minirootXX.img, and cdXX.iso targets are unchanged.  Enhanced targets are
+     available only through Makefile.rdsshd.
+
+SYNOPSIS
+     Run the obj target separately, then build with at least one authorized
+     public key:
+
+           make -f Makefile.rdsshd obj
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+SOURCE LAYOUT
+     The patch adds one source directory containing eleven files:
+
+           distrib/amd64/ramdisk_cd/rdsshd/Makefile.rdsshd
+           distrib/amd64/ramdisk_cd/rdsshd/Makefile.init
+           distrib/amd64/ramdisk_cd/rdsshd/README.rdsshd
+           distrib/amd64/ramdisk_cd/rdsshd/console-lock.sshd.patch
+           distrib/amd64/ramdisk_cd/rdsshd/init.sshd.patch
+           distrib/amd64/ramdisk_cd/rdsshd/installer.sshd.patch
+           distrib/amd64/ramdisk_cd/rdsshd/list.sshd
+           distrib/amd64/ramdisk_cd/rdsshd/root.profile
+           distrib/amd64/ramdisk_cd/rdsshd/sshd_config
+           distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD
+           distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD_NO_PROPOLICE
+
+TARGETS
+     rdsshd
+             Builds bsdsshd.rd and minirootXX_sshd.img.
+
+     install-rdsshd
+             Defined when RELEASEDIR is set.  Installs compressed bsdsshd.rd
+             and minirootXX_sshd.img:
+
+                   make -f Makefile.rdsshd install-rdsshd \
+                       RELEASEDIR=/path/to/release
+
+     unconfig-rdsshd
+             Unmounts the private mount point and detaches its recorded vnd
+             after an interrupted miniroot build.
+
+     clean-rdsshd, clean, cleandir
+             Remove private rdsshd outputs.  Stock instbin objects remain under
+             normal OpenBSD clean ownership.
+
+     prepare-unpatch-rdsshd
+             Runs private cleanup, prints the wrapper object path, and removes
+             the rdsshd source obj symlink.
+
+BUILD MODEL
+     Private paths below the rdsshd object directory:
+
+           obj/build/kernel       enhanced kernel objects
+           obj/build/instbin      copy of stock instbin
+           obj/sshobj             static OpenSSH objects
+           obj/initsrc            optional patched init source
+           obj/initobj            optional private init objects
+           obj/stage              generated and overlay files
+           obj/rdobj              ramdisk assembly
+           obj/bsd*               copied enhanced kernels
+           obj/boot               miniroot boot file
+           obj/mnt                miniroot mount point
+           obj/vnd                vnd ownership record
+
+     The kernel is configured with config(8) -b and built below
+     obj/build/kernel.  No sys/arch/amd64/compile directory is added or used.
+     OpenSSH uses explicit private MAKEOBJDIR paths.
+
+     instbin is unmodified.  Its normal objects remain under:
+
+           ${BSDOBJDIR}/distrib/amd64/ramdisk_cd
+           ${BSDOBJDIR}/distrib/special
+           ${BSDOBJDIR}/lib
+
+     The wrapper runs the normal obj targets for lib, distrib/special, and
+     ramdisk_cd.  It builds stock distrib/special/libstubs, then invokes the
+     stock ramdisk_cd instbin target.  That target builds component objects and
+     reduced source libraries.  The wrapper does not transform crunchgen
+     configuration or generated Makefiles.  It copies the completed instbin to
+     obj/build/instbin before ramdisk assembly.
+
+     BSDOBJDIR must exist and retain normal OpenBSD ownership and permissions.
+     The default is /usr/obj, owned by build:wobj with mode 770.  The build
+     rejects source-directory object fallback for both the wrapper and
+     ramdisk_cd.
+
+     Normal clean and cleandir targets own the shared instbin objects.
+     clean-rdsshd owns only the private copy and enhanced outputs.  Do not run
+     rdsshd concurrently with a normal build or clean using the same instbin
+     objects.
+
+     No parent Makefile or SUBDIR list is changed.  Normal top-level builds and
+     cleans do not enter the rdsshd source directory.
+
+     Normal and enhanced media use separate boot files, mount points, image
+     names, and vnd state.  Cleanup detaches only the recorded vnd covering the
+     absolute enhanced image path.  A missing, malformed, or reused vnd is
+     reported and left attached.
+
+KERNEL
+     RAMDISK_CD_SSHD includes stock RAMDISK_CD, then applies:
+
+           rmoption NO_PROPOLICE
+           rmoption MINIROOTSIZE
+           option MINIROOTSIZE=40960
+           pseudo-device pty 16
+
+     Removing NO_PROPOLICE enables normal kernel stack protection.
+     RDSSHD_KERNEL_STACK_PROTECTOR=no selects
+     RAMDISK_CD_SSHD_NO_PROPOLICE, which retains NO_PROPOLICE.
+
+     MINIROOTSIZE=40960 reserves a 20 MiB rdroot.  Sixteen ptys support ssh
+     sessions.  SMALL_KERNEL remains enabled.
+
+     A temporary RDSSHD_RDROOT option protects config(8)'s option-list append
+     pointer while inherited tail entries are removed.  It is absent from the
+     final configuration.
+
+RAMDISK
+     Stock bsd.rd uses the host disktab entry rdrootb:
+
+           MRMAKEFSARGS=-o disklabel=rdrootb,minfree=0,density=4096
+
+     bsdsshd.rd instead gives makefs(8) an explicit 20 MiB layout:
+
+           -s 20m \
+             -o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
+
+     This does not read or modify /etc/disktab.
+
+     Stock installer programs remain in instbin.  OpenSSH is built through its
+     normal Makefiles as separate static PIE executables with zlib disabled:
+
+           /usr/sbin/sshd
+           /usr/bin/ssh-keygen
+           /usr/libexec/sshd-session
+           /usr/libexec/sshd-auth
+
+     The ssh(1) client is not included.
+
+     list.sshd adds:
+
+           /etc/ssh/sshd_config
+           /root/.ssh/authorized_keys
+           /root/.profile
+           /auto_install.conf
+           /etc/login.conf
+
+     It also adds the sshd account and pty devices.  Root retains stock
+     instbin's -sh argv link.  root.profile leaves sh mode, sets the installer
+     environment, and sets TERM=vt220.  The Port directive in sshd_config is
+     substituted only in private staging.
+
+     installer.sshd.patch modifies private copies of stock .profile and
+     install.sub.  console-lock.sshd.patch adds the locked early profile path.
+     init.sshd.patch applies only to a private init.c copy.  patch(1) runs
+     non-interactively with zero fuzz.  Context drift fails before replacement.
+
+     Image layout:
+
+           embedded rdroot                 20 MiB
+           outer miniroot                  32768 x 512 bytes (16 MiB)
+
+     The outer image follows the stock amd64 miniroot layout.  It installs BIOS
+     and EFI boot files using private boot, mount, image, and vnd paths.
+
+     minirootXX_sshd.img stores compressed bsdsshd.gz as /bsd.  It does not
+     store uncompressed bsdsshd.rd.
+
+     Example amd64 artifact sizes:
+
+           boot                    87 KiB
+           bsdsshd.gz             9.0 MiB
+           bsdsshd.rd            27.2 MiB
+           bsdsshd.strip         26.7 MiB
+           minirootXX_sshd.img   16.0 MiB
+
+     These are observations, not fixed limits.  Console locking adds a private
+     static init and may increase the compressed kernel size.
+
+SSHD
+     sshd permits public-key authentication for root only.  Relevant policy:
+
+           AllowUsers root
+           PermitRootLogin prohibit-password
+           PubkeyAuthentication yes
+           AuthenticationMethods publickey
+           PasswordAuthentication no
+           KbdInteractiveAuthentication no
+           Compression no
+           PermitUserRC no
+           PrintMotd no
+           PrintLastLog no
+           DisableForwarding yes
+
+     The static PIE executables retain normal OpenBSD OpenSSH compiler and
+     linker protections.  Compression is disabled at build and run time.
+
+     RDSSHD_AUTHORIZED_KEYS is mandatory.  It may contain one or more public
+     keys.  Blank and comment lines are removed.  Every remaining line must
+     pass ssh-keygen(1) public-key validation.  The installed file has mode
+     0600; /root and /root/.ssh have mode 0700.
+
+     No host key is embedded by default.  During the build, ssh-keygen creates
+     obj/stage/ssh_host_ed25519_key.test and its public key solely to validate
+     the generated configuration with the newly built sshd -t.  The build
+     removes the test configuration and key pair after validation.  They never
+     enter the ramdisk.  An interrupted build may leave them below obj/stage;
+     the next staging pass or cleandir removes them.
+
+     On first boot, the ramdisk generates:
+
+           /etc/ssh/ssh_host_ed25519_key
+
+     Normal installer CGI fetches call feed_random before generation.  An
+     unreachable fetch delays sshd until the normal CGI timeout.
+
+     RDSSHD_HOST_KEY may name an Ed25519 host private key without a passphrase.
+     The build validates it and copies it to private staging with mode 0600.
+     The newly built sshd validates the generated configuration against this
+     staged key.  Build-host ssh configuration and keys are not read or
+     modified.
+
+     An embedded key is recoverable from private objects, kernels, images, and
+     release copies.  Key-bearing kernel and media artifacts have mode 0600.
+
+     sshd starts after donetconfig.  A live numeric PID greater than one in
+     /var/run/sshd.pid suppresses restart.  The ramdisk lacks a process
+     inspection utility.  This checks liveness, not executable identity.
+
+     install.sub creates /var/run/rdsshd.ready only after sshd starts.
+     Bootstrap profiles use this marker because installer exit status alone
+     does not establish readiness.
+
+CONSOLE AND INSTALLER
+     The initial profile invokes:
+
+           install -af /auto_install.conf
+
+     /auto_install.conf answers only the early network questions.  Static IPv4
+     configuration includes netmask and default route.  autoconf, dhcp, and
+     none omit them.
+
+     With RDSSHD_CONSOLE_LOCK=no, automatic setup begins after the normal menu
+     timeout unless a local operator selects another action.  install.sub exits
+     after starting sshd.  The profile then returns to the local ramdisk shell.
+
+     With RDSSHD_CONSOLE_LOCK=yes, a private static init replaces /sbin/init in
+     the enhanced ramdisk.  Makefile.init invokes the stock special/init
+     Makefile, supplies stock pathnames.h, and keeps patched source and objects
+     private.  The bootstrap child retains inherited /dev/null descriptors and
+     never acquires /dev/console as a controlling terminal.
+
+     The locked profile branches before terminal setup.  It retries automatic
+     network and sshd setup until /var/run/rdsshd.ready exists, then sleeps.
+     It never presents a menu or shell.  Init restarts it after exit.
+     RDSSHD_CONSOLE_LOCK=no uses stock init from instbin.
+
+     After ssh login, run:
+
+           install
+
+     This starts the interactive installer.  At network prompts, choose done to
+     retain the active configuration.  Reconfiguration can drop the ssh
+     session.
+
+CAVEATS
+     RDSSHD_CONSOLE_LOCK removes the interactive local installer userland.
+     Boot-loader and kernel consoles remain active.  A local operator can still
+     change early boot state, reset, halt, or deny remote access.
+
+     RDSSHD_KERNEL_STACK_PROTECTOR=no deliberately retains NO_PROPOLICE.  This
+     may reduce image size and weakens mitigation of kernel stack corruption.
+
+     Protect RDSSHD_HOST_KEY, the object tree, all key-bearing images, and
+     installed copies.  Use a distinct host key for each machine identity.
+
+     RDSSHD_FSSIZE is not auto-sized.  A value too small for the compressed
+     kernel and boot files causes miniroot assembly to fail.
+
+     Network variables become installer response-file answers.  The build does
+     not probe the interface or validate network reachability.  Incorrect
+     values can prevent ssh access.  A locked image then has no local installer
+     userland with which to repair the configuration.
+
+BUILD
+     Build on OpenBSD/amd64 with src.tar.gz and sys.tar.gz matching the
+     installed OpenBSD release.  Normal source-build prerequisites apply.  The
+     media target requires root privileges for vnconfig(8), mount operations,
+     device access, and file ownership.  Kernel compilation runs as BUILDUSER.
+
+     Apply the patch:
+
+           cd /usr/src
+           patch -p1 < /root/bsdsshd.rd.patch
+
+     The configured BSDOBJDIR root, normally /usr/obj, must already exist.
+     Build from the added directory:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd obj
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+     Run obj separately.  OpenBSD make selects .OBJDIR at startup and rejects a
+     combined obj and rdsshd invocation.  No top-level /usr/src make obj is
+     required.  The wrapper creates only the normal object links required by
+     stock instbin.  Custom components retain private paths.
+
+     Remove the local installer session:
+
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
+               RDSSHD_CONSOLE_LOCK=yes
+
+     Retain NO_PROPOLICE:
+
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
+               RDSSHD_KERNEL_STACK_PROTECTOR=no
+
+     Embed a stable host key:
+
+           ssh-keygen -q -t ed25519 -N '' \
+               -f /root/rdsshd_host_ed25519_key
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
+               RDSSHD_HOST_KEY=/root/rdsshd_host_ed25519_key
+
+     Outputs:
+
+           /usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/bsdsshd.rd
+           /usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/minirootXX_sshd.img
+
+REBUILD AND CLEAN
+     For variable or key changes, repeat rdsshd.  Normal dependency rules reuse
+     current stock instbin components, static OpenSSH, private init, and the
+     selected kernel.  Staging, ramdisk, and media are regenerated.
+
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+     After source, compiler, flag, or Makefile changes, clean both ownership
+     domains:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd
+           make cleandir
+           cd /usr/src/distrib/special
+           make cleandir
+           cd /usr/src/lib
+           make cleandir
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd cleandir
+           make -f Makefile.rdsshd obj
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+     A top-level cleandir may replace the three stock cleans.  The next rdsshd
+     build recreates required normal object directories.  The wrapper clean
+     never removes stock objects.
+
+     After a failed stock instbin trace link, clean ramdisk_cd before retrying.
+     Its instbin.map and reduced archives may be incomplete:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd
+           make cleandir
+
+     After an interrupted miniroot build:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd unconfig-rdsshd
+
+VARIABLES
+     RDSSHD_AUTHORIZED_KEYS
+             Required file containing one or more root authorized public keys.
+
+     RDSSHD_HOST_KEY
+             Optional Ed25519 host private key without a passphrase.  An empty
+             value generates a new host key at each boot.  Default: empty.
+
+     RDSSHD_CONSOLE_LOCK
+             yes selects private init and removes the local installer session.
+             no retains the local menu and shell.  Default: no.
+
+     RDSSHD_KERNEL_STACK_PROTECTOR
+             yes removes inherited NO_PROPOLICE.  no retains it.  Default: yes.
+
+     RDSSHD_PORT
+             sshd listen port.  The generated sshd_config must pass sshd -t.
+             Default: 22.
+
+     RDSSHD_AI_IF
+             Network interface.  Default: em0.
+
+     RDSSHD_AI_HOSTNAME
+             Hostname.  Default: rdinstall.
+
+     RDSSHD_AI_IPV4
+             IPv4 address or installer keyword.  Default: autoconf.
+
+     RDSSHD_AI_NETMASK
+             Static IPv4 netmask.  Default: 255.255.255.0.
+
+     RDSSHD_AI_ROUTE
+             Static IPv4 default route.  Default: none.
+
+     RDSSHD_AI_IPV6
+             IPv6 address or installer keyword.  Default: none.
+
+     RDSSHD_AI_DOMAIN
+             DNS domain.  Default: my.domain.
+
+     RDSSHD_AI_DNS
+             DNS nameservers.  Default: none.
+
+     RDSSHD_FSSIZE
+             Outer miniroot size in 512-byte blocks.  Default: 32768 (16 MiB).
+
+     RDSSHD_MRMAKEFSARGS
+             makefs(8) arguments for the embedded rdroot.  The default creates
+             a 20 MiB filesystem matching MINIROOTSIZE=40960.  An override must
+             fit the kernel reservation.
+
+     BSDOBJDIR
+             Normal OpenBSD object root used by stock instbin.  Default:
+             /usr/obj.
+
+     DESTDIR
+             Optional prefix for installed amd64 boot files under usr/mdec.
+             Default: empty.
+
+     RELEASEDIR
+             Enables install-rdsshd and names its destination directory.
+
+     Boolean values are case-insensitive.  Values other than yes and no are
+     rejected.
+
+USE
+     Prepare a non-interactive next boot from the running system.
+     No boot prompt or console access is assumed.
+
+     Whole-device image
+             minirootXX_sshd.img is a complete disk image.  Writing it to a
+             block device replaces the device's partition table and filesystems.
+             Verify the output device before writing it.  For example, on Linux:
+
+                   dd if=./miniroot79_sshd.img of=/dev/sda bs=512
+                   sync
+                   reboot
+
+             The running OS or storage stack may deny raw writes to the initial
+             sectors of the device backing its active root filesystem.  Use the
+             boot-loader method if this cannot be changed remotely.
+
+             The image boots in UEFI or BIOS/CSM mode with Secure Boot disabled.
+             Firmware must already select the target device.  The rdroot runs
+             from memory, so the installer can reuse that device as its target.
+
+     GRUB one-shot boot
+             Copy the uncompressed ramdisk kernel.  Do not rely on gzio:
+
+                   cp /path/to/bsdsshd.rd /boot/bsdsshd.rd
+
+             Add to /etc/grub.d/40_custom:
+
+                   menuentry "OpenBSD bsdsshd.rd" {
+                           insmod part_gpt
+                           insmod ext2
+                           insmod bsd
+                           # Replace UUID with the /boot filesystem UUID.
+                           search --no-floppy --fs-uuid --set=root UUID
+                           kopenbsd /bsdsshd.rd
+                   }
+
+             Set in /etc/default/grub:
+
+                   GRUB_DEFAULT=saved
+
+             Select a one-shot boot:
+
+                   update-grub
+                   grub-reboot "OpenBSD bsdsshd.rd"
+                   grub-editenv list
+                   sync
+                   reboot
+
+             This example was tested in BIOS/CSM mode.  UEFI is untested and
+             may lack display console output.  Secure Boot is unsupported.
+             part_gpt and ext2 match the tested /boot filesystem; change them
+             for the target layout.  The GRUB build must provide the bsd module
+             and kopenbsd command.  Other boot loaders are untested.
+
+     Network booting
+             The matching OpenBSD/amd64 pxeboot(8) can load the compressed
+             bsdsshd.rd over the network in place of bsd.rd.
+
+     Remote session
+             After the enhanced kernel boots, connect when the configured
+             address accepts ssh:
+
+                   ssh -i /path/to/private_key root@host.example
+                   install
+
+             Perform required storage preparation before running install.  At
+             network prompts, choose done to retain the active connection.
+
+             RDSSHD_CONSOLE_LOCK=yes presents no local installer menu or shell.
+
+REMOVE PATCH
+     Remove private outputs and the wrapper obj symlink before reversal:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd prepare-unpatch-rdsshd
+           cd /usr/src
+           patch -R -p1 < /root/bsdsshd.rd.patch
+           rmdir /usr/obj/distrib/amd64/ramdisk_cd/rdsshd
+           rmdir /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+
+     prepare-unpatch-rdsshd prints the recorded object path before removing the
+     symlink.  /usr/obj is the default; use the printed path when different.
+     patch(1) removes added files but leaves their empty parent directory.
+     rmdir refuses non-empty directories.
+
+FILES
+     /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+             Added source directory and Makefile.rdsshd entry point.
+
+     obj/bsdsshd.rd
+             Uncompressed enhanced ramdisk kernel.
+
+     obj/bsdsshd.gz
+             Stripped and compressed ramdisk kernel installed as bsdsshd.rd by
+             install-rdsshd and as /bsd in the enhanced miniroot.
+
+     obj/minirootXX_sshd.img
+             Enhanced BIOS- and EFI-bootable miniroot image.
+
+     obj/vnd
+             Private ownership record for an attached vnd during media
+             assembly.  Removed after successful assembly or safe cleanup.
+
+DIAGNOSTICS
+     private object directory is not active
+             Run make -f Makefile.rdsshd obj as a separate invocation before
+             building.
+
+     normal OpenBSD object root does not exist
+             Create BSDOBJDIR with normal OpenBSD ownership and permissions.
+
+     normal ramdisk_cd object directory is not active
+             The stock obj target did not select an object directory distinct
+             from its source directory.  Check BSDOBJDIR and rerun the build.
+
+     stock ramdisk_cd instbin was not built
+             The stock instbin target did not produce its expected output.
+             Inspect the preceding failure.  Clean ramdisk_cd before retrying
+             after a failed trace link.
+
+     invalid public key on line N of RDSSHD_AUTHORIZED_KEYS
+             Replace the indicated line with a public key accepted by
+             ssh-keygen(1).
+
+     RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key
+             Supply an Ed25519 host private key without a passphrase.
+
+     stale private vnd state
+             Run unconfig-rdsshd before rebuilding the miniroot.
+
+     refusing to detach vnd not owned by rdsshd
+             The recorded device no longer covers the enhanced image.  The
+             target leaves it attached for manual inspection.
+
+SEE ALSO
+     make(1), patch(1), ssh(1), ssh-keygen(1), softraid(4), vnd(4), disktab(5),
+     install.site(5), sshd_config(5), autoinstall(8), bioctl(8), boot(8),
+     boot_amd64(8), config(8), installboot(8), makefs(8), rdsetroot(8),
+     release(8), sshd(8), vnconfig(8)
+
+BSDSSHD.RD                      July 18, 2026                       BSDSSHD.RD
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/console-lock.sshd.patch
@@ -0,0 +1,18 @@
+--- dot.profile.tmp
++++ dot.profile.tmp
+@@ -87,6 +87,15 @@
+ 	[[ -x /sbin/dhcpleased ]] && /sbin/dhcpleased 2>/dev/null
+ 	[[ -x /sbin/slaacd ]] && /sbin/slaacd 2>/dev/null
+ 
++	while [[ ! -f /var/run/rdsshd.ready ]]; do
++		/install -af /auto_install.conf
++		sleep 1
++	done
++
++	while :; do
++		sleep 3600
++	done
++
+ 	# Set up some sane tty defaults.
+ 	echo 'erase ^?, werase ^W, kill ^U, intr ^C, status ^T'
+ 	stty newcrt werase ^W intr ^C kill ^U erase ^? status ^T
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/init.sshd.patch
@@ -0,0 +1,20 @@
+--- init.c.tmp
++++ init.c.tmp
+@@ -517,7 +517,6 @@
+ 		/*
+ 		 * Start the single user session.
+ 		 */
+-		setctty(_PATH_CONSOLE);
+ 
+ #ifdef SECURE
+ 		/*
+@@ -651,8 +650,7 @@
+ 		}
+ 	}
+ 
+-	runcom_mode = FASTBOOT;
+-	return runcom;
++	return single_user;
+ }
+ 
+ /*
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/installer.sshd.patch
@@ -0,0 +1,71 @@
+--- install.sub.tmp
++++ install.sub.tmp
+@@ -1377,7 +1377,10 @@
+ 		ask_until "$_q (name, lladdr, '?', or 'done')" \
+ 		    ${_p:-$( (get_ifs netboot; get_ifs) | sed q )}
+ 
+-		[[ $resp == done ]] && break
++		if [[ $resp == done ]]; then
++			NIFS=$(ls -1 /tmp/i/hostname.* 2>/dev/null | grep -c ^)
++			break
++		fi
+ 		[[ $resp == '?'  ]] && continue
+ 
+ 		# Quote $resp to prevent user from confusing isin() by
+@@ -3170,6 +3173,21 @@
+ 	echo "\nConfiguring the root disk $ROOTDISK...\n"
+ }
+ 
++rdsshd_start() {
++	local _pid
++	if [[ -s /var/run/sshd.pid ]]; then
++		_pid=$(</var/run/sshd.pid)
++		[[ $_pid == +([0-9]) ]] && (( _pid > 1 )) && \
++		    kill -0 "$_pid" 2>/dev/null && return 0
++	fi
++	if [[ ! -s /etc/ssh/ssh_host_ed25519_key ]]; then
++		/usr/bin/ssh-keygen -q -t ed25519 -N "" \
++		    -f /etc/ssh/ssh_host_ed25519_key || return 1
++	fi
++	/usr/sbin/sshd -t -f /etc/ssh/sshd_config || return 1
++	/usr/sbin/sshd -f /etc/ssh/sshd_config || return 1
++}
++
+ do_install() {
+ 	local _rootkey _rootpass
+ 
+@@ -3190,7 +3208,20 @@
+ 
+ 	# Configure the network.
+ 	donetconfig
++	if $AI && [[ $AI_RESPFILE == /auto_install.conf ]]; then
++		start_cgiinfo
++		wait_cgiinfo
++		rdsshd_start || err_exit "Could not start ramdisk sshd."
++		>/var/run/rdsshd.ready
++		cat <<__EOT
+ 
++ramdisk sshd is running.
++Connect as root with the matching key.
++Run: install
++__EOT
++		exit 0
++	fi
++
+ 	# Fetch list of mirror servers and installer choices from previous runs.
+ 	start_cgiinfo
+ 
+--- dot.profile.tmp
++++ dot.profile.tmp
+@@ -130,6 +130,11 @@
+ 		if $timeout; then
+ 			timeout=false
+ 			echo
++			if [[ -f /auto_install.conf ]]; then
++				/install -af /auto_install.conf
++				[[ -f /var/run/rdsshd.ready ]] && break
++				continue
++			fi
+ 			REPLY=a
+ 		else
+ 			# User has made a choice; stop the read timeout.
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/list.sshd
@@ -0,0 +1,35 @@
+#	$OpenBSD$
+
+# bsdsshd.rd overlay.
+MKDIR	usr/libexec
+MKDIR	etc/ssh
+MKDIR	root
+MKDIR	root/.ssh
+
+COPY	${OBJDIR}/../stage/sshd_config			etc/ssh/sshd_config
+SPECIAL test ! -s ${OBJDIR}/../stage/ssh_host_ed25519_key || install -c -m 600 -o root -g wheel ${OBJDIR}/../stage/ssh_host_ed25519_key etc/ssh/ssh_host_ed25519_key
+COPY	${OBJDIR}/../stage/auto_install.conf		auto_install.conf
+SCRIPT	${OBJDIR}/../stage/dot.profile			.profile
+SCRIPT	${OBJDIR}/../stage/install.sub			install.sub
+SPECIAL	chmod 755 install.sub
+
+COPY	${OBJDIR}/../stage/master.passwd		etc/master.passwd
+COPY	${OBJDIR}/../stage/group			etc/group
+SPECIAL	pwd_mkdb -p -d etc master.passwd; rm etc/master.passwd
+
+COPY	${CURDIR}/rdsshd/root.profile			root/.profile
+COPY	${OBJDIR}/../stage/authorized_keys		root/.ssh/authorized_keys
+SPECIAL	chmod 700 root root/.ssh; chmod 600 root/.ssh/authorized_keys
+
+SPECIAL	cd dev; sh MAKEDEV pty0 ptm
+
+COPY	${OBJDIR}/../stage/bin/sshd			usr/sbin/sshd
+SPECIAL	chmod 511 usr/sbin/sshd
+COPY	${OBJDIR}/../stage/bin/ssh-keygen		usr/bin/ssh-keygen
+SPECIAL	chmod 555 usr/bin/ssh-keygen
+COPY	${OBJDIR}/../stage/bin/sshd-session		usr/libexec/sshd-session
+SPECIAL	chmod 511 usr/libexec/sshd-session
+COPY	${OBJDIR}/../stage/bin/sshd-auth		usr/libexec/sshd-auth
+SPECIAL	chmod 511 usr/libexec/sshd-auth
+
+COPY	${CURDIR}/../../../etc/etc.amd64/login.conf	etc/login.conf
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/root.profile
@@ -0,0 +1,16 @@
+set +o sh
+export VNAME=$(sysctl -n kern.osrelease)
+export VERSION="${VNAME%.*}${VNAME#*.}"
+export ARCH=$(sysctl -n hw.machine)
+export OBSD="OpenBSD/$ARCH $VNAME"
+PATH=/sbin:/bin:/usr/bin:/usr/sbin:/
+export PATH
+TERM=vt220
+export TERM
+umask 022
+set -o emacs
+PS1='rd# '
+export PS1
+echo
+echo "ramdisk sshd is running."
+echo "Run install to continue."
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/sshd_config
@@ -0,0 +1,14 @@
+Port ${RDSSHD_PORT}
+HostKey /etc/ssh/ssh_host_ed25519_key
+AllowUsers root
+PermitRootLogin prohibit-password
+AuthorizedKeysFile .ssh/authorized_keys
+PubkeyAuthentication yes
+AuthenticationMethods publickey
+PasswordAuthentication no
+KbdInteractiveAuthentication no
+Compression no
+PermitUserRC no
+PrintMotd no
+PrintLastLog no
+DisableForwarding yes
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD
@@ -0,0 +1,12 @@
+#	$OpenBSD$
+
+include "arch/amd64/conf/RAMDISK_CD"
+
+# Keep config(8)'s option append pointer valid while removing the tail.
+option		RDSSHD_RDROOT
+rmoption	NO_PROPOLICE
+rmoption	MINIROOTSIZE
+option		MINIROOTSIZE=40960
+rmoption	RDSSHD_RDROOT
+
+pseudo-device	pty	16
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD_NO_PROPOLICE
@@ -0,0 +1,11 @@
+#	$OpenBSD$
+
+include "arch/amd64/conf/RAMDISK_CD"
+
+# Keep config(8)'s option append pointer valid while removing the tail.
+option		RDSSHD_RDROOT
+rmoption	MINIROOTSIZE
+option		MINIROOTSIZE=40960
+rmoption	RDSSHD_RDROOT
+
+pseudo-device	pty	16

SYNOPSIS

Run obj separately. Supply a file containing at least one authorized public key.

Build invocationraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

SOURCE LAYOUT

The patch adds one source directory containing eleven files.

Added filesraw
distrib/amd64/ramdisk_cd/rdsshd/Makefile.rdsshd
distrib/amd64/ramdisk_cd/rdsshd/Makefile.init
distrib/amd64/ramdisk_cd/rdsshd/README.rdsshd
distrib/amd64/ramdisk_cd/rdsshd/console-lock.sshd.patch
distrib/amd64/ramdisk_cd/rdsshd/init.sshd.patch
distrib/amd64/ramdisk_cd/rdsshd/installer.sshd.patch
distrib/amd64/ramdisk_cd/rdsshd/list.sshd
distrib/amd64/ramdisk_cd/rdsshd/root.profile
distrib/amd64/ramdisk_cd/rdsshd/sshd_config
distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD
distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD_NO_PROPOLICE

TARGETS

rdsshd
Builds both enhanced images.
install-rdsshd
Defined when RELEASEDIR is set. Installs compressed bsdsshd.rd and minirootXX_sshd.img.
unconfig-rdsshd
Unmounts the private mount point and detaches its recorded vnd after an interrupted miniroot build.
clean-rdsshd, clean, cleandir
Remove private outputs. Normal OpenBSD clean targets retain ownership of stock instbin objects.
prepare-unpatch-rdsshd
Cleans private outputs, prints the wrapper object path, and removes the source obj symlink.
Install release filesraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd install-rdsshd \
    RELEASEDIR=/path/to/release
Makefile.rdsshdEnhanced wrapper and targetsrawโ†“
#	$OpenBSD$

.include <bsd.own.mk>

RDSSHD_AUTHORIZED_KEYS?=
RDSSHD_HOST_KEY?=
RDSSHD_CONSOLE_LOCK?=	no
RDSSHD_KERNEL_STACK_PROTECTOR?=	yes
RDSSHD_PORT?=	22
RDSSHD_AI_IF?=	em0
RDSSHD_AI_HOSTNAME?=	rdinstall
RDSSHD_AI_IPV4?=	autoconf
RDSSHD_AI_NETMASK?=	255.255.255.0
RDSSHD_AI_ROUTE?=	none
RDSSHD_AI_IPV6?=	none
RDSSHD_AI_DOMAIN?=	my.domain
RDSSHD_AI_DNS?=	none
RDSSHD_FSSIZE?=	32768

RDSSHD_BASEDIR=	${.CURDIR}/..
RDSSHD_TOP=	${.CURDIR}/../../../..
RDSSHD_UTILS=	${RDSSHD_BASEDIR}/../../miniroot
RDSSHD_MTREE=	${RDSSHD_UTILS}/mtree.conf
RDSSHD_EFIBOOT=	${DESTDIR}/usr/mdec/BOOTX64.EFI \
		${DESTDIR}/usr/mdec/BOOTIA32.EFI
RDSSHD_MOUNT_ARGS_MSDOS=	-o-s

RDSSHD_RAMDISK=	RAMDISK_CD_SSHD
RDSSHD_KERNEL=	${.OBJDIR}/bsd
.if ${RDSSHD_KERNEL_STACK_PROTECTOR:L} == "no"
RDSSHD_RAMDISK=	RAMDISK_CD_SSHD_NO_PROPOLICE
RDSSHD_KERNEL=	${.OBJDIR}/bsd.no-propolice
.endif
RDSSHD_FS=	miniroot${OSrev}_sshd.img
RDSSHD_BUILDOBJDIR=	${.OBJDIR}/build
RDSSHD_KERNELOBJDIR=	${RDSSHD_BUILDOBJDIR}/kernel/${RDSSHD_RAMDISK}
RDSSHD_INSTBIN=	${RDSSHD_BUILDOBJDIR}/instbin
RDSSHD_STAGE=	${.OBJDIR}/stage
RDSSHD_RDOBJDIR=	${.OBJDIR}/rdobj
RDSSHD_SSHOBJDIR=	${.OBJDIR}/sshobj
RDSSHD_INITOBJDIR=	${.OBJDIR}/initobj
RDSSHD_INITSRCDIR=	${.OBJDIR}/initsrc
RDSSHD_MOUNT_POINT=	${.OBJDIR}/mnt
RDSSHD_VND=	${.OBJDIR}/vnd
RDSSHD_BOOT=	${.OBJDIR}/boot
RDSSHD_OBJCHECK=	${.OBJDIR}/.rdsshd-obj-ok
RDSSHD_MAKEFILE=	${.CURDIR}/Makefile.rdsshd
RDSSHD_INITMAKEFILE=	${.CURDIR}/Makefile.init
RDSSHD_KERNELCONF=	${.CURDIR}/${RDSSHD_RAMDISK}
RDSSHD_BASECONF=	${RDSSHD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD
RDSSHD_KERNELCONFDEPS=	${RDSSHD_BASECONF}
RDSSHD_LISTS=	${RDSSHD_BASEDIR}/list ${.CURDIR}/list.sshd \
		${RDSSHD_STAGE}/list.console
RDSSHD_INITSRC=	${RDSSHD_INITSRCDIR}/init.c
RDSSHD_INIT=	${RDSSHD_STAGE}/init
RDSSHD_INSTALLERPATCH=	${.CURDIR}/installer.sshd.patch
RDSSHD_CONSOLEPATCH=	${.CURDIR}/console-lock.sshd.patch
RDSSHD_INITPATCH=	${.CURDIR}/init.sshd.patch
RDSSHD_SSHDCONF=	${.CURDIR}/sshd_config
RDSSHD_MRMAKEFSARGS?=	-s 20m \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
RDSSHD_BINS=	${RDSSHD_STAGE}/bin/sshd \
		${RDSSHD_STAGE}/bin/ssh-keygen \
		${RDSSHD_STAGE}/bin/sshd-session \
		${RDSSHD_STAGE}/bin/sshd-auth

RDSSHD_INITDEP=
.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
RDSSHD_INITDEP=	${RDSSHD_INIT}
.endif

.PHONY: rdsshd
rdsshd: bsdsshd.rd ${RDSSHD_FS}

.PHONY: rdsshd-objcheck
rdsshd-objcheck: ${RDSSHD_OBJCHECK}

${RDSSHD_OBJCHECK}:
	@if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \
		echo "private object directory is not active;" >&2; \
		echo "run 'make -f Makefile.rdsshd obj' separately first" >&2; \
		exit 1; \
	fi
	touch $@

${RDSSHD_KERNEL}: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE} \
	    ${RDSSHD_KERNELCONF} ${RDSSHD_KERNELCONFDEPS}
	install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${RDSSHD_KERNELOBJDIR}
	su ${BUILDUSER} -c \
	    'config -b ${RDSSHD_KERNELOBJDIR} -s ${RDSSHD_TOP}/sys \
	    ${RDSSHD_KERNELCONF} && cd ${RDSSHD_KERNELOBJDIR} && \
	    MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} clean && \
	    exec env MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'
	cp -p ${RDSSHD_KERNELOBJDIR}/bsd $@

bsdsshd.gz: bsdsshd.rd
	objcopy -g -x -R .comment -R .SUNW_ctf \
	    -K rd_root_size -K rd_root_image \
	    bsdsshd.rd bsdsshd.strip
	gzip -9cn bsdsshd.strip > bsdsshd.gz
.if !empty(RDSSHD_HOST_KEY)
	chmod 600 bsdsshd.strip $@
.endif

.PHONY: rdsshd-stock-deps rdsshd-instbin
rdsshd-stock-deps: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	@_objroot=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \
	if [ ! -d "$$_objroot" ]; then \
		echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \
		echo "create it before building rdsshd" >&2; \
		exit 1; \
	fi
	cd ${RDSSHD_TOP}/lib && ${MAKE} obj
	cd ${RDSSHD_TOP}/distrib/special && ${MAKE} obj
	cd ${RDSSHD_BASEDIR} && ${MAKE} obj
	@_srcdir=`cd ${RDSSHD_BASEDIR} && pwd`; \
	_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
	if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \
		echo "normal ramdisk_cd object directory is not active" >&2; \
		exit 1; \
	fi
	cd ${RDSSHD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}

rdsshd-instbin: rdsshd-stock-deps
	cd ${RDSSHD_BASEDIR} && ${MAKE} ${MFLAGS} instbin
	install -d ${RDSSHD_BUILDOBJDIR}
	@_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
	if [ ! -f "$$_objdir/instbin" ]; then \
		echo "stock ramdisk_cd instbin was not built" >&2; \
		exit 1; \
	fi; \
	cp -p "$$_objdir/instbin" ${RDSSHD_INSTBIN}

bsdsshd.rd: rdsshd-files ${RDSSHD_INITDEP} rdsshd-instbin \
	    ${RDSSHD_KERNEL}
	install -d ${RDSSHD_RDOBJDIR}
	rm -f ${RDSSHD_RDOBJDIR}/instbin \
	    ${RDSSHD_RDOBJDIR}/mr.fs \
	    ${RDSSHD_RDOBJDIR}/bsd.rd
	cp -p ${RDSSHD_INSTBIN} ${RDSSHD_RDOBJDIR}/instbin
	rm -rf ${RDSSHD_RDOBJDIR}/mr.fs.d
	install -d -o root -g wheel ${RDSSHD_RDOBJDIR}/mr.fs.d
	mtree -def ${RDSSHD_MTREE} -p ${RDSSHD_RDOBJDIR}/mr.fs.d -u
	CURDIR=${RDSSHD_BASEDIR} OBJDIR=${RDSSHD_RDOBJDIR} OSrev=${OSrev} \
	    TARGDIR=${RDSSHD_RDOBJDIR}/mr.fs.d UTILS=${RDSSHD_UTILS} \
	    RELEASEDIR=${RELEASEDIR} sh ${RDSSHD_UTILS}/runlist.sh \
	    ${RDSSHD_LISTS}
	rm ${RDSSHD_RDOBJDIR}/mr.fs.d/instbin
	makefs ${RDSSHD_MRMAKEFSARGS} ${RDSSHD_RDOBJDIR}/mr.fs \
	    ${RDSSHD_RDOBJDIR}/mr.fs.d
	cp -p ${RDSSHD_KERNEL} ${RDSSHD_RDOBJDIR}/bsd.rd
	rdsetroot ${RDSSHD_RDOBJDIR}/bsd.rd ${RDSSHD_RDOBJDIR}/mr.fs
	cp ${RDSSHD_RDOBJDIR}/bsd.rd $@
.if !empty(RDSSHD_HOST_KEY)
	chmod 600 ${RDSSHD_RDOBJDIR}/mr.fs \
	    ${RDSSHD_RDOBJDIR}/bsd.rd $@
.endif

${RDSSHD_FS}: bsdsshd.gz
	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
		rm -f ${RDSSHD_VND}; \
	fi
	@if [ -e ${RDSSHD_VND} ]; then \
		echo "stale private vnd state; run" \
		    "'make -f Makefile.rdsshd unconfig-rdsshd' first" >&2; \
		exit 1; \
	fi
	install -d ${RDSSHD_MOUNT_POINT}
	dd if=/dev/zero of=${RDSSHD_FS} bs=512 count=${RDSSHD_FSSIZE}
	vnconfig -v ${.OBJDIR}/${RDSSHD_FS} > ${RDSSHD_VND}
	fdisk -yi -l ${RDSSHD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
	    `cat ${RDSSHD_VND}`
	echo '/ *' | disklabel -wAT- `cat ${RDSSHD_VND}`
	newfs -t msdos /dev/r`cat ${RDSSHD_VND}`i
	mount ${RDSSHD_MOUNT_ARGS_MSDOS} /dev/`cat ${RDSSHD_VND}`i \
	    ${RDSSHD_MOUNT_POINT}
	mkdir -p ${RDSSHD_MOUNT_POINT}/efi/boot
	cp ${RDSSHD_EFIBOOT} ${RDSSHD_MOUNT_POINT}/efi/boot
	umount ${RDSSHD_MOUNT_POINT}
	newfs -O 1 -m 0 -o space -i 524288 -c ${RDSSHD_FSSIZE} \
	    /dev/r`cat ${RDSSHD_VND}`a
	mount /dev/`cat ${RDSSHD_VND}`a ${RDSSHD_MOUNT_POINT}
	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${RDSSHD_BOOT}
	installboot -v -r ${RDSSHD_MOUNT_POINT} `cat ${RDSSHD_VND}` \
	    ${DESTDIR}/usr/mdec/biosboot ${RDSSHD_BOOT}
	install -c -m 555 -o root -g wheel bsdsshd.gz \
	    ${RDSSHD_MOUNT_POINT}/bsd
	df -i ${RDSSHD_MOUNT_POINT}
	umount ${RDSSHD_MOUNT_POINT}
	vnconfig -u `cat ${RDSSHD_VND}`
	rm -f ${RDSSHD_VND}
.if !empty(RDSSHD_HOST_KEY)
	chmod 600 $@
.endif

.PHONY: rdsshd-check rdsshd-files rdsshd-scripts
rdsshd-check:
	@if [ -z "${RDSSHD_AUTHORIZED_KEYS}" ]; then \
		echo "set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2; \
		exit 1; \
	fi
	@case "${RDSSHD_CONSOLE_LOCK:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_CONSOLE_LOCK must be yes or no" >&2; exit 1;; \
	esac
	@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2; exit 1;; \
	esac

${RDSSHD_INITSRC}: ${RDSSHD_OBJCHECK} ${RDSSHD_TOP}/sbin/init/init.c \
	    ${RDSSHD_INITPATCH}
	install -d ${RDSSHD_INITSRCDIR}
	rm -f ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}.tmp.orig \
	    ${RDSSHD_INITSRC}.tmp.rej
	cp ${RDSSHD_TOP}/sbin/init/init.c ${RDSSHD_INITSRC}.tmp
	cd ${RDSSHD_INITSRCDIR} && \
	    patch -f -s -p0 -F0 < ${RDSSHD_INITPATCH}
	mv ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}

${RDSSHD_INIT}: ${RDSSHD_INITSRC} \
	    ${RDSSHD_TOP}/sbin/init/pathnames.h ${RDSSHD_INITMAKEFILE} \
	    ${RDSSHD_BASEDIR}/../../special/init/Makefile \
	    ${RDSSHD_BASEDIR}/../../special/Makefile.inc
	install -d ${RDSSHD_INITOBJDIR} ${RDSSHD_STAGE}
	cd ${RDSSHD_BASEDIR}/../../special/init && \
	    MAKEOBJDIR=${RDSSHD_INITOBJDIR} ${MAKE} ${MFLAGS} \
	    -f ${RDSSHD_INITMAKEFILE} \
	    RDSSHD_INITSRCDIR=${RDSSHD_INITSRCDIR} init
	install -c -s ${RDSSHD_INITOBJDIR}/init $@

rdsshd-scripts: ${RDSSHD_OBJCHECK} ${RDSSHD_UTILS}/install.sub \
	    ${RDSSHD_UTILS}/dot.profile \
	    ${RDSSHD_INSTALLERPATCH} ${RDSSHD_CONSOLEPATCH}
	install -d ${RDSSHD_STAGE}
	rm -f ${RDSSHD_STAGE}/install.sub.tmp \
	    ${RDSSHD_STAGE}/install.sub.tmp.orig \
	    ${RDSSHD_STAGE}/install.sub.tmp.rej \
	    ${RDSSHD_STAGE}/dot.profile.tmp \
	    ${RDSSHD_STAGE}/dot.profile.tmp.orig \
	    ${RDSSHD_STAGE}/dot.profile.tmp.rej
	cp ${RDSSHD_UTILS}/install.sub ${RDSSHD_STAGE}/install.sub.tmp
	cp ${RDSSHD_UTILS}/dot.profile ${RDSSHD_STAGE}/dot.profile.tmp
	cd ${RDSSHD_STAGE} && \
	    patch -f -s -p0 -F0 < ${RDSSHD_INSTALLERPATCH}
.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
	cd ${RDSSHD_STAGE} && \
	    patch -f -s -p0 -F0 < ${RDSSHD_CONSOLEPATCH}
.endif
	chmod 755 ${RDSSHD_STAGE}/install.sub.tmp
	mv ${RDSSHD_STAGE}/install.sub.tmp ${RDSSHD_STAGE}/install.sub
	mv ${RDSSHD_STAGE}/dot.profile.tmp ${RDSSHD_STAGE}/dot.profile

rdsshd-files: rdsshd-check rdsshd-scripts ${RDSSHD_BINS} \
	    ${RDSSHD_SSHDCONF}
	install -d ${RDSSHD_STAGE}
	@if [ "${RDSSHD_CONSOLE_LOCK:L}" = yes ]; then \
		echo 'COPY ${RDSSHD_INIT} sbin/init'; \
	fi > ${RDSSHD_STAGE}/list.console
	sed '/^root:/s|:/bin/ksh$$|:/bin/sh|' \
	    ${RDSSHD_UTILS}/master.passwd > ${RDSSHD_STAGE}/master.passwd
	grep '^root:.*:/bin/sh$$' ${RDSSHD_STAGE}/master.passwd >/dev/null
	sed -e '/^[	 ]*$$/d' -e '/^[	 ]*#/d' \
	    < "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
	test -s ${RDSSHD_STAGE}/authorized_keys
	@_n=0; while IFS= read -r _key; do \
		_n=$$((_n + 1)); \
		if ! printf '%s\n' "$$_key" | \
		    ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \
			echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \
			exit 1; \
		fi; \
	done < ${RDSSHD_STAGE}/authorized_keys
	grep '^sshd:' ${RDSSHD_STAGE}/master.passwd >/dev/null || \
	    grep '^sshd:' ${RDSSHD_TOP}/etc/master.passwd >> \
	    ${RDSSHD_STAGE}/master.passwd
	cp ${RDSSHD_UTILS}/group ${RDSSHD_STAGE}/group
	grep '^sshd:' ${RDSSHD_STAGE}/group >/dev/null || \
	    grep '^sshd:' ${RDSSHD_TOP}/etc/group >> ${RDSSHD_STAGE}/group
	{ \
	    printf 'System hostname = %s\n' '${RDSSHD_AI_HOSTNAME}'; \
	    printf 'Network interface to configure = %s\n' '${RDSSHD_AI_IF}'; \
	    printf 'IPv4 address for %s = %s\n' \
		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV4}'; \
	    case '${RDSSHD_AI_IPV4}' in \
	    none|autoconf|dhcp) ;; \
	    *) printf 'Netmask for %s = %s\n' \
		'${RDSSHD_AI_IF}' '${RDSSHD_AI_NETMASK}'; \
	       printf 'Default IPv4 route = %s\n' '${RDSSHD_AI_ROUTE}' ;; \
	    esac; \
	    printf 'IPv6 address for %s = %s\n' \
		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV6}'; \
	    printf 'Network interface to configure = done\n'; \
	    printf 'DNS domain name = %s\n' '${RDSSHD_AI_DOMAIN}'; \
	    printf 'DNS nameservers = %s\n' '${RDSSHD_AI_DNS}'; \
	} > ${RDSSHD_STAGE}/auto_install.conf
	sed 's|^Port .*|Port ${RDSSHD_PORT}|' ${RDSSHD_SSHDCONF} > \
	    ${RDSSHD_STAGE}/sshd_config
	rm -f ${RDSSHD_STAGE}/ssh_host_ed25519_key \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.pub \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test.pub \
	    ${RDSSHD_STAGE}/sshd_config.test
	@if [ -n "${RDSSHD_HOST_KEY}" ]; then \
		if ! ${RDSSHD_STAGE}/bin/ssh-keygen -y -P '' \
		    -f "${RDSSHD_HOST_KEY}" 2>/dev/null | grep -q '^ssh-ed25519 '; then \
			echo "RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key" >&2; \
			exit 1; \
		fi; \
		install -c -m 600 "${RDSSHD_HOST_KEY}" \
		    ${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	fi
	@_key=${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	_testkey=${RDSSHD_STAGE}/ssh_host_ed25519_key.test; \
	_testconf=${RDSSHD_STAGE}/sshd_config.test; \
	if [[ ! -s $$_key ]]; then \
		_key=$$_testkey; \
		${RDSSHD_STAGE}/bin/ssh-keygen -q -t ed25519 -N '' \
		    -f $$_key || exit 1; \
	fi; \
	sed "s|^HostKey .*|HostKey $$_key|" \
	    ${RDSSHD_STAGE}/sshd_config > $$_testconf; \
	${RDSSHD_STAGE}/bin/sshd -t -f $$_testconf; \
	_status=$$?; \
	rm -f $$_testconf $$_testkey $$_testkey.pub; \
	exit $$_status
${RDSSHD_STAGE}/bin/sshd: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd
	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/sshd/sshd $@

${RDSSHD_STAGE}/bin/ssh-keygen: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/ssh-keygen
	cd ${RDSSHD_TOP}/usr.bin/ssh/ssh-keygen && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/ssh-keygen ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/ssh-keygen/ssh-keygen $@

${RDSSHD_STAGE}/bin/sshd-session: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-session
	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-session && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-session ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-session/sshd-session $@

${RDSSHD_STAGE}/bin/sshd-auth: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-auth
	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-auth && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-auth ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-auth/sshd-auth $@

.PHONY: unconfig-rdsshd
unconfig-rdsshd:
	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
		rm -f ${RDSSHD_VND}; \
	elif [ -f ${RDSSHD_VND} ]; then \
		_vnd=`cat ${RDSSHD_VND}`; \
		_unit=$${_vnd#vnd}; \
		if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \
			echo "invalid private vnd state: $$_vnd" >&2; exit 1; \
		fi; \
		case "$$_unit" in \
		*[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \
		esac; \
		_info=`vnconfig -l "$$_vnd"` || exit 1; \
		case "$$_info" in \
		"$$_vnd: not in use") rm -f ${RDSSHD_VND} ;; \
		"$$_vnd: covering ${.OBJDIR}/${RDSSHD_FS} on "*) \
			vnconfig -u "$$_vnd" && rm -f ${RDSSHD_VND} ;; \
		*) echo "refusing to detach vnd not owned by rdsshd: $$_info" >&2; \
			exit 1 ;; \
		esac; \
	fi

.ifdef RELEASEDIR
.PHONY: install-rdsshd
install-rdsshd: bsdsshd.gz ${RDSSHD_FS}
	cp bsdsshd.gz ${RELEASEDIR}/bsdsshd.rd
	cp ${RDSSHD_FS} ${RELEASEDIR}
.if empty(RDSSHD_HOST_KEY)
	chmod a+r ${RELEASEDIR}/bsdsshd.rd
.else
	chmod 600 ${RELEASEDIR}/bsdsshd.rd ${RELEASEDIR}/${RDSSHD_FS}
.endif
.endif

.PHONY: clean-rdsshd clean cleandir
clean-rdsshd: unconfig-rdsshd
	rm -f bsdsshd.rd bsdsshd.gz bsdsshd.strip ${RDSSHD_FS} \
	    ${.OBJDIR}/bsd ${.OBJDIR}/bsd.no-propolice ${RDSSHD_BOOT} \
	    ${RDSSHD_OBJCHECK}
	rm -rf ${RDSSHD_BUILDOBJDIR} ${RDSSHD_STAGE} ${RDSSHD_RDOBJDIR} \
	    ${RDSSHD_SSHOBJDIR} ${RDSSHD_INITOBJDIR} ${RDSSHD_INITSRCDIR}
	-rmdir ${RDSSHD_MOUNT_POINT}

clean cleandir: clean-rdsshd

.PHONY: prepare-unpatch-rdsshd
prepare-unpatch-rdsshd: clean-rdsshd
	@if [ -L ${.CURDIR}/obj ]; then \
		_obj=`readlink ${.CURDIR}/obj`; \
		echo "empty private object directory may be removed: $$_obj"; \
		rm -f ${.CURDIR}/obj; \
	fi

.include <bsd.obj.mk>

BUILD MODEL

Private objects

Object ownershipraw
Private rdsshd paths:

obj/build/kernel       enhanced kernel objects
obj/build/instbin      copy of stock instbin
obj/sshobj             static OpenSSH objects
obj/initsrc            optional patched init source
obj/initobj            optional private init objects
obj/stage              generated and overlay files
obj/rdobj              ramdisk assembly
obj/bsd*               copied enhanced kernels
obj/boot               miniroot boot file
obj/mnt                miniroot mount point
obj/vnd                vnd ownership record

Normal OpenBSD paths used by stock instbin:

${BSDOBJDIR}/distrib/amd64/ramdisk_cd
${BSDOBJDIR}/distrib/special
${BSDOBJDIR}/lib

The enhanced kernel, OpenSSH programs, optional init, staging tree, ramdisk, media, mount point, and vnd record remain private. The kernel uses config(8) -b. OpenSSH uses explicit private MAKEOBJDIR paths. No directory below sys/arch/amd64/compile is added or used.

Stock instbin

Installer instbin is unmodified. The wrapper runs the normal obj targets for lib, distrib/special, and ramdisk_cd. It builds stock distrib/special/libstubs, invokes the stock instbin target, then copies the result to obj/build/instbin below the private object directory.

Component objects, reduced libraries, and crunchgen output retain their normal paths below ${BSDOBJDIR}. The wrapper does not rewrite generated crunchgen files. Normal OpenBSD clean targets own these stock objects. clean-rdsshd owns the private copy and enhanced outputs only. Enhanced and normal builds or cleans must not run concurrently when they share instbin objects.

No parent Makefile or SUBDIR list is changed. Normal top-level builds and cleans do not enter the rdsshd source directory.

Object checks and media state

BSDOBJDIR must exist with normal OpenBSD ownership and permissions. The default is /usr/obj, owned by build:wobj with mode 770. The build rejects source-directory fallback for both the wrapper and ramdisk_cd.

Normal and enhanced media use distinct boot files, image names, mount points, and vnd state. Cleanup detaches only the recorded vnd covering the absolute enhanced image path. A missing, malformed, or reused vnd is reported and retained.

KERNEL

RAMDISK_CD_SSHD includes stock RAMDISK_CD. It changes only the inherited stack-protection option, rdroot reservation, and pty count.

RAMDISK_CD_SSHDraw
#	$OpenBSD$

include "arch/amd64/conf/RAMDISK_CD"

# Keep config(8)'s option append pointer valid while removing the tail.
option		RDSSHD_RDROOT
rmoption	NO_PROPOLICE
rmoption	MINIROOTSIZE
option		MINIROOTSIZE=40960
rmoption	RDSSHD_RDROOT

pseudo-device	pty	16

Removing NO_PROPOLICE enables normal kernel stack protection. RDSSHD_KERNEL_STACK_PROTECTOR=no selects a separate overlay that retains NO_PROPOLICE.

RAMDISK_CD_SSHD_NO_PROPOLICEraw
#	$OpenBSD$

include "arch/amd64/conf/RAMDISK_CD"

# Keep config(8)'s option append pointer valid while removing the tail.
option		RDSSHD_RDROOT
rmoption	MINIROOTSIZE
option		MINIROOTSIZE=40960
rmoption	RDSSHD_RDROOT

pseudo-device	pty	16

MINIROOTSIZE=40960 reserves a 20 MiB rdroot. Sixteen ptys support ssh sessions. SMALL_KERNEL remains enabled.

The temporary RDSSHD_RDROOT option protects config(8)'s option-list append pointer while inherited tail entries are removed. It is absent from the final configuration.

RAMDISK

Embedded filesystem

Stock bsd.rd uses the host disktab entry rdrootb.

Stock rdrootraw
MRMAKEFSARGS=-o disklabel=rdrootb,minfree=0,density=4096

The enhanced target gives makefs(8) an explicit 20 MiB layout. It does not read or modify /etc/disktab.

Enhanced rdrootraw
RDSSHD_MRMAKEFSARGS?=	-s 20m \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096

Programs and files

Stock installer programs remain in instbin. OpenSSH is built through its normal Makefiles as separate static PIE executables with zlib disabled. The ssh(1) client is not included.

The separate executables are /usr/sbin/sshd, /usr/bin/ssh-keygen, /usr/libexec/sshd-session, and /usr/libexec/sshd-auth.

Ramdisk additionsrawโ†“
#	$OpenBSD$

# bsdsshd.rd overlay.
MKDIR	usr/libexec
MKDIR	etc/ssh
MKDIR	root
MKDIR	root/.ssh

COPY	${OBJDIR}/../stage/sshd_config			etc/ssh/sshd_config
SPECIAL test ! -s ${OBJDIR}/../stage/ssh_host_ed25519_key || install -c -m 600 -o root -g wheel ${OBJDIR}/../stage/ssh_host_ed25519_key etc/ssh/ssh_host_ed25519_key
COPY	${OBJDIR}/../stage/auto_install.conf		auto_install.conf
SCRIPT	${OBJDIR}/../stage/dot.profile			.profile
SCRIPT	${OBJDIR}/../stage/install.sub			install.sub
SPECIAL	chmod 755 install.sub

COPY	${OBJDIR}/../stage/master.passwd		etc/master.passwd
COPY	${OBJDIR}/../stage/group			etc/group
SPECIAL	pwd_mkdb -p -d etc master.passwd; rm etc/master.passwd

COPY	${CURDIR}/rdsshd/root.profile			root/.profile
COPY	${OBJDIR}/../stage/authorized_keys		root/.ssh/authorized_keys
SPECIAL	chmod 700 root root/.ssh; chmod 600 root/.ssh/authorized_keys

SPECIAL	cd dev; sh MAKEDEV pty0 ptm

COPY	${OBJDIR}/../stage/bin/sshd			usr/sbin/sshd
SPECIAL	chmod 511 usr/sbin/sshd
COPY	${OBJDIR}/../stage/bin/ssh-keygen		usr/bin/ssh-keygen
SPECIAL	chmod 555 usr/bin/ssh-keygen
COPY	${OBJDIR}/../stage/bin/sshd-session		usr/libexec/sshd-session
SPECIAL	chmod 511 usr/libexec/sshd-session
COPY	${OBJDIR}/../stage/bin/sshd-auth		usr/libexec/sshd-auth
SPECIAL	chmod 511 usr/libexec/sshd-auth

COPY	${CURDIR}/../../../etc/etc.amd64/login.conf	etc/login.conf

The overlay adds sshd configuration, authorized keys, the root profile, login classes, early network answers, the sshd account, and pty devices. Root retains stock instbin's -sh argv link. The root profile leaves sh mode, sets the installer environment, and selects TERM=vt220. The sshd Port directive is substituted only in private staging.

Root profileraw
set +o sh
export VNAME=$(sysctl -n kern.osrelease)
export VERSION="${VNAME%.*}${VNAME#*.}"
export ARCH=$(sysctl -n hw.machine)
export OBSD="OpenBSD/$ARCH $VNAME"
PATH=/sbin:/bin:/usr/bin:/usr/sbin:/
export PATH
TERM=vt220
export TERM
umask 022
set -o emacs
PS1='rd# '
export PS1
echo
echo "ramdisk sshd is running."
echo "Run install to continue."

Overlay patches

Each patch applies only to a private copy of a stock file. patch(1) runs non-interactively with zero fuzz. Context drift fails before replacement.

Installer integrationrawโ†“
--- install.sub.tmp
+++ install.sub.tmp
@@ -1377,7 +1377,10 @@
 		ask_until "$_q (name, lladdr, '?', or 'done')" \
 		    ${_p:-$( (get_ifs netboot; get_ifs) | sed q )}
 
-		[[ $resp == done ]] && break
+		if [[ $resp == done ]]; then
+			NIFS=$(ls -1 /tmp/i/hostname.* 2>/dev/null | grep -c ^)
+			break
+		fi
 		[[ $resp == '?'  ]] && continue
 
 		# Quote $resp to prevent user from confusing isin() by
@@ -3170,6 +3173,21 @@
 	echo "\nConfiguring the root disk $ROOTDISK...\n"
 }
 
+rdsshd_start() {
+	local _pid
+	if [[ -s /var/run/sshd.pid ]]; then
+		_pid=$(</var/run/sshd.pid)
+		[[ $_pid == +([0-9]) ]] && (( _pid > 1 )) && \
+		    kill -0 "$_pid" 2>/dev/null && return 0
+	fi
+	if [[ ! -s /etc/ssh/ssh_host_ed25519_key ]]; then
+		/usr/bin/ssh-keygen -q -t ed25519 -N "" \
+		    -f /etc/ssh/ssh_host_ed25519_key || return 1
+	fi
+	/usr/sbin/sshd -t -f /etc/ssh/sshd_config || return 1
+	/usr/sbin/sshd -f /etc/ssh/sshd_config || return 1
+}
+
 do_install() {
 	local _rootkey _rootpass
 
@@ -3190,7 +3208,20 @@
 
 	# Configure the network.
 	donetconfig
+	if $AI && [[ $AI_RESPFILE == /auto_install.conf ]]; then
+		start_cgiinfo
+		wait_cgiinfo
+		rdsshd_start || err_exit "Could not start ramdisk sshd."
+		>/var/run/rdsshd.ready
+		cat <<__EOT
 
+ramdisk sshd is running.
+Connect as root with the matching key.
+Run: install
+__EOT
+		exit 0
+	fi
+
 	# Fetch list of mirror servers and installer choices from previous runs.
 	start_cgiinfo
 
--- dot.profile.tmp
+++ dot.profile.tmp
@@ -130,6 +130,11 @@
 		if $timeout; then
 			timeout=false
 			echo
+			if [[ -f /auto_install.conf ]]; then
+				/install -af /auto_install.conf
+				[[ -f /var/run/rdsshd.ready ]] && break
+				continue
+			fi
 			REPLY=a
 		else
 			# User has made a choice; stop the read timeout.
Locked profile pathraw
--- dot.profile.tmp
+++ dot.profile.tmp
@@ -87,6 +87,15 @@
 	[[ -x /sbin/dhcpleased ]] && /sbin/dhcpleased 2>/dev/null
 	[[ -x /sbin/slaacd ]] && /sbin/slaacd 2>/dev/null
 
+	while [[ ! -f /var/run/rdsshd.ready ]]; do
+		/install -af /auto_install.conf
+		sleep 1
+	done
+
+	while :; do
+		sleep 3600
+	done
+
 	# Set up some sane tty defaults.
 	echo 'erase ^?, werase ^W, kill ^U, intr ^C, status ^T'
 	stty newcrt werase ^W intr ^C kill ^U erase ^? status ^T
Private init behaviorraw
--- init.c.tmp
+++ init.c.tmp
@@ -517,7 +517,6 @@
 		/*
 		 * Start the single user session.
 		 */
-		setctty(_PATH_CONSOLE);
 
 #ifdef SECURE
 		/*
@@ -651,8 +650,7 @@
 		}
 	}
 
-	runcom_mode = FASTBOOT;
-	return runcom;
+	return single_user;
 }
 
 /*

Miniroot media

The outer image follows the stock amd64 BIOS and EFI miniroot layout. It uses private boot, mount, image, and vnd paths. The default size is 32768 512-byte blocks, or 16 MiB.

BIOS and EFI media assemblyraw
	dd if=/dev/zero of=${RDSSHD_FS} bs=512 count=${RDSSHD_FSSIZE}
	vnconfig -v ${.OBJDIR}/${RDSSHD_FS} > ${RDSSHD_VND}
	fdisk -yi -l ${RDSSHD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
	    `cat ${RDSSHD_VND}`
	echo '/ *' | disklabel -wAT- `cat ${RDSSHD_VND}`
	newfs -t msdos /dev/r`cat ${RDSSHD_VND}`i
	mount ${RDSSHD_MOUNT_ARGS_MSDOS} /dev/`cat ${RDSSHD_VND}`i \
	    ${RDSSHD_MOUNT_POINT}
	mkdir -p ${RDSSHD_MOUNT_POINT}/efi/boot
	cp ${RDSSHD_EFIBOOT} ${RDSSHD_MOUNT_POINT}/efi/boot
	umount ${RDSSHD_MOUNT_POINT}
	newfs -O 1 -m 0 -o space -i 524288 -c ${RDSSHD_FSSIZE} \
	    /dev/r`cat ${RDSSHD_VND}`a
	mount /dev/`cat ${RDSSHD_VND}`a ${RDSSHD_MOUNT_POINT}
	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${RDSSHD_BOOT}
	installboot -v -r ${RDSSHD_MOUNT_POINT} `cat ${RDSSHD_VND}` \
	    ${DESTDIR}/usr/mdec/biosboot ${RDSSHD_BOOT}
	install -c -m 555 -o root -g wheel bsdsshd.gz \
	    ${RDSSHD_MOUNT_POINT}/bsd

The miniroot stores compressed bsdsshd.gz as /bsd. It does not store uncompressed bsdsshd.rd.

Observed sizes

These amd64 sizes are observations from a default build, not fixed limits. Console locking may increase the compressed kernel size.

Example artifactsraw
boot                    87 KiB
bsdsshd.gz             9.0 MiB
bsdsshd.rd            27.2 MiB
bsdsshd.strip         26.7 MiB
minirootXX_sshd.img   16.0 MiB

SSHD

Authentication policy

sshd permits root public-key authentication only. Forwarding, user rc, interactive authentication, passwords, and compression are disabled. Static PIE executables retain normal OpenBSD OpenSSH compiler and linker protections.

sshd_configraw
Port ${RDSSHD_PORT}
HostKey /etc/ssh/ssh_host_ed25519_key
AllowUsers root
PermitRootLogin prohibit-password
AuthorizedKeysFile .ssh/authorized_keys
PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
Compression no
PermitUserRC no
PrintMotd no
PrintLastLog no
DisableForwarding yes

Authorized keys

RDSSHD_AUTHORIZED_KEYS is mandatory. It may contain one or more public keys. Blank and comment lines are removed. Every retained line must pass ssh-keygen(1) public-key validation.

The installed file has mode 0600. /root and /root/.ssh have mode 0700.

authorized_keys validationraw
	sed -e '/^[	 ]*$$/d' -e '/^[	 ]*#/d' \
	    < "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
	test -s ${RDSSHD_STAGE}/authorized_keys
	@_n=0; while IFS= read -r _key; do \
		_n=$$((_n + 1)); \
		if ! printf '%s\n' "$$_key" | \
		    ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \
			echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \
			exit 1; \
		fi; \
	done < ${RDSSHD_STAGE}/authorized_keys

Build-time configuration test

If RDSSHD_HOST_KEY is empty, the build generates obj/stage/ssh_host_ed25519_key.test and its public key. These files exist only to give the newly built sshd -t a host key while it validates the generated configuration.

The build removes the test configuration and key pair after validation. They never enter the ramdisk. An interrupted build may leave them below obj/stage; the next staging pass or cleandir removes them.

Host-key and sshd_config validationrawโ†“
	rm -f ${RDSSHD_STAGE}/ssh_host_ed25519_key \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.pub \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test.pub \
	    ${RDSSHD_STAGE}/sshd_config.test
	@if [ -n "${RDSSHD_HOST_KEY}" ]; then \
		if ! ${RDSSHD_STAGE}/bin/ssh-keygen -y -P '' \
		    -f "${RDSSHD_HOST_KEY}" 2>/dev/null | grep -q '^ssh-ed25519 '; then \
			echo "RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key" >&2; \
			exit 1; \
		fi; \
		install -c -m 600 "${RDSSHD_HOST_KEY}" \
		    ${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	fi
	@_key=${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	_testkey=${RDSSHD_STAGE}/ssh_host_ed25519_key.test; \
	_testconf=${RDSSHD_STAGE}/sshd_config.test; \
	if [[ ! -s $$_key ]]; then \
		_key=$$_testkey; \
		${RDSSHD_STAGE}/bin/ssh-keygen -q -t ed25519 -N '' \
		    -f $$_key || exit 1; \
	fi; \
	sed "s|^HostKey .*|HostKey $$_key|" \
	    ${RDSSHD_STAGE}/sshd_config > $$_testconf; \
	${RDSSHD_STAGE}/bin/sshd -t -f $$_testconf; \
	_status=$$?; \
	rm -f $$_testconf $$_testkey $$_testkey.pub; \
	exit $$_status
Host-key and sshd_config validation1.1 KiBrawโ†‘

Boot-generated host key

No host key is embedded by default. First boot generates /etc/ssh/ssh_host_ed25519_key. Normal installer CGI fetches call feed_random before generation. An unreachable fetch delays sshd until the normal CGI timeout.

Embedded host key

RDSSHD_HOST_KEY may name an Ed25519 host private key without a passphrase. The build validates it and copies it to private staging with mode 0600. The newly built sshd validates the generated configuration against that staged key. Build-host ssh configuration and keys are not read or modified.

An embedded key is recoverable from private objects, kernels, images, and release copies. Key-bearing kernel and media artifacts use mode 0600.

Startup

sshd starts after donetconfig. A live numeric PID greater than one in /var/run/sshd.pid suppresses restart. The ramdisk has no process-inspection utility, so this checks liveness only.

install.sub creates /var/run/rdsshd.ready only after sshd starts. Profiles use this marker because installer exit status does not establish readiness.

sshd startuprawโ†“
rdsshd_start() {
	local _pid
	if [[ -s /var/run/sshd.pid ]]; then
		_pid=$(</var/run/sshd.pid)
		[[ $_pid == +([0-9]) ]] && (( _pid > 1 )) && \
		    kill -0 "$_pid" 2>/dev/null && return 0
	fi
	if [[ ! -s /etc/ssh/ssh_host_ed25519_key ]]; then
		/usr/bin/ssh-keygen -q -t ed25519 -N "" \
		    -f /etc/ssh/ssh_host_ed25519_key || return 1
	fi
	/usr/sbin/sshd -t -f /etc/ssh/sshd_config || return 1
	/usr/sbin/sshd -f /etc/ssh/sshd_config || return 1
}

donetconfig
if $AI && [[ $AI_RESPFILE == /auto_install.conf ]]; then
	start_cgiinfo
	wait_cgiinfo
	rdsshd_start || err_exit "Could not start ramdisk sshd."
	>/var/run/rdsshd.ready
	cat <<__EOT

ramdisk sshd is running.
Connect as root with the matching key.
Run: install
__EOT
	exit 0
fi

CONSOLE AND INSTALLER

Network bootstrap

The initial profile invokes install -af with early network answers. Static IPv4 configuration includes a netmask and default route. autoconf, dhcp, and none omit them.

Static IPv4 response fileDocumentation addresses use RFC 5737 spaceraw
System hostname = rdinstall
Network interface to configure = em0
IPv4 address for em0 = 192.0.2.10
Netmask for em0 = 255.255.255.0
Default IPv4 route = 192.0.2.1
IPv6 address for em0 = none
Network interface to configure = done
DNS domain name = example.com
DNS nameservers = 192.0.2.53

Default console

Automatic setup starts after the normal installer-menu timeout unless a local operator selects another action. The profile returns to the local ramdisk shell after sshd starts.

Default profile pathraw
if [[ -f /auto_install.conf ]]; then
	/install -af /auto_install.conf
	[[ -f /var/run/rdsshd.ready ]] && break
	continue
fi

Locked console

With RDSSHD_CONSOLE_LOCK=yes, a private static init replaces /sbin/init only in the enhanced ramdisk. It is built through the stock distrib/special/init Makefile using the dedicated wrapper and stock pathnames.h. Patched source and objects stay private.

Private init wrapperraw
#	$OpenBSD$

.PATH: ${RDSSHD_INITSRCDIR}
CPPFLAGS+=	-I${.CURDIR}/../../../sbin/init
.include "${.CURDIR}/Makefile"

The bootstrap child retains inherited /dev/null descriptors. It never acquires /dev/console as a controlling terminal. The profile branches before terminal setup, retries setup until the ready marker exists, then sleeps. It presents no menu or shell. Init restarts it after exit. The default uses stock init from instbin.

Locked profile pathraw
while [[ ! -f /var/run/rdsshd.ready ]]; do
	/install -af /auto_install.conf
	sleep 1
done

while :; do
	sleep 3600
done

Remote installer

After ssh login, run install. Complete storage preparation first. At network prompts, select done to retain the active configuration. Reconfiguration can drop the ssh session.

CAVEATS

Console locking removes the interactive local installer userland. The boot-loader and kernel consoles remain active. A local operator can still change early boot state, reset, halt, or deny remote access.

RDSSHD_KERNEL_STACK_PROTECTOR=no deliberately retains NO_PROPOLICE. This may reduce image size and weakens mitigation of kernel stack corruption.

Protect an embedded host key, the object tree, every key-bearing image, and installed copies. Use a distinct host key for each machine identity.

RDSSHD_FSSIZE is not auto-sized. A value too small for the compressed kernel and boot files causes miniroot assembly to fail.

Network variables become installer response-file answers. The build does not probe the interface or validate reachability. Incorrect values can prevent ssh access. A locked image then has no local installer userland with which to repair the configuration.

BUILD

Prerequisites and base build

Build on OpenBSD/amd64 with src.tar.gz and sys.tar.gz matching the installed OpenBSD release. Normal source-build prerequisites apply. The media target requires root for vnd, mount, device, and ownership operations. Kernel compilation runs as BUILDUSER.

Apply the patch below /usr/src. The configured BSDOBJDIR root, normally /usr/obj, must already exist.

Base buildraw
cd /usr/src
patch -p1 < /root/bsdsshd.rd.patch

cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

Run obj separately. OpenBSD make selects .OBJDIR at startup and rejects a combined obj rdsshd invocation. No top-level /usr/src make obj is required. The wrapper creates only the normal object links required by stock instbin. Custom components retain private paths.

Static network

Supply netmask and route with a static IPv4 address. The documentation addresses below use the reserved 192.0.2.0/24 range.

Static IPv4 buildraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_AI_IF=em0 \
    RDSSHD_AI_HOSTNAME=rdinstall \
    RDSSHD_AI_IPV4=192.0.2.10 \
    RDSSHD_AI_NETMASK=255.255.255.0 \
    RDSSHD_AI_ROUTE=192.0.2.1 \
    RDSSHD_AI_IPV6=none \
    RDSSHD_AI_DOMAIN=example.com \
    RDSSHD_AI_DNS=192.0.2.53

Console lock

Remove local installer sessionraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_CONSOLE_LOCK=yes

Kernel without stack protection

Retain NO_PROPOLICEraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_KERNEL_STACK_PROTECTOR=no

Embedded host key

Generate and embed host keyraw
ssh-keygen -q -t ed25519 -N '' -f /root/rdsshd_host_ed25519_key
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_HOST_KEY=/root/rdsshd_host_ed25519_key

Use a distinct host key per machine identity. Protect its source, object tree, and all resulting images. Derive its public-key fingerprint before deployment and compare it when establishing host trust.

Host-key fingerprintraw
ssh-keygen -y -f /root/rdsshd_host_ed25519_key |
    ssh-keygen -lf -

Outputs

Primary build outputsraw
/usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/bsdsshd.rd
/usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/minirootXX_sshd.img

REBUILD AND CLEAN

Variable or key changes

Repeat rdsshd. Normal dependency rules reuse current stock instbin components, OpenSSH, private init, and the selected kernel. Staging, ramdisk, and media are regenerated.

Rebuild variables or keysraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

Source or toolchain changes

Clean both ownership domains after source, compiler, flag, or Makefile changes. A top-level cleandir may replace the three stock cleans. The next enhanced build recreates required normal object directories. The wrapper clean never removes stock objects.

Rebuild changed sourcesraw
cd /usr/src/distrib/amd64/ramdisk_cd
make cleandir
cd /usr/src/distrib/special
make cleandir
cd /usr/src/lib
make cleandir

cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd cleandir
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

Clean ramdisk_cd before retrying. Its instbin.map and reduced archives may be incomplete.

Clean stock instbin stateraw
cd /usr/src/distrib/amd64/ramdisk_cd
make cleandir

Interrupted media build

Release only the private mount and the vnd recorded as covering the enhanced image.

Release private media stateraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd unconfig-rdsshd

VARIABLES

RDSSHD_AUTHORIZED_KEYS
Required file containing one or more root authorized public keys.
RDSSHD_HOST_KEY
Optional Ed25519 host private key without a passphrase. An empty value generates a key at each boot. Default: empty.
RDSSHD_CONSOLE_LOCK
yes removes the local installer session. no retains the menu and shell. Default: no.
RDSSHD_KERNEL_STACK_PROTECTOR
yes removes inherited NO_PROPOLICE. no retains it. Default: yes.
RDSSHD_PORT
sshd port. The generated configuration must pass sshd -t. Default: 22.
RDSSHD_AI_IF
Network interface. Default: em0.
RDSSHD_AI_HOSTNAME
Hostname. Default: rdinstall.
RDSSHD_AI_IPV4
IPv4 address or installer keyword. Default: autoconf.
RDSSHD_AI_NETMASK
Static IPv4 netmask. Default: 255.255.255.0.
RDSSHD_AI_ROUTE
Static IPv4 default route. Default: none.
RDSSHD_AI_IPV6
IPv6 address or installer keyword. Default: none.
RDSSHD_AI_DOMAIN
DNS domain. Default: my.domain.
RDSSHD_AI_DNS
DNS nameservers. Default: none.
RDSSHD_FSSIZE
Outer miniroot size in 512-byte blocks. Default: 32768 (16 MiB).
RDSSHD_MRMAKEFSARGS
Embedded-rdroot makefs(8) arguments. The default creates a 20 MiB filesystem matching MINIROOTSIZE=40960. An override must fit the kernel reservation.
BSDOBJDIR
Normal object root used by stock instbin. Default: /usr/obj.
DESTDIR
Optional prefix for installed amd64 boot files below usr/mdec. Default: empty.
RELEASEDIR
Enables install-rdsshd and names its destination.

Boolean values are case-insensitive. Values other than yes and no are rejected.

Defaults and validationrawโ†“
RDSSHD_AUTHORIZED_KEYS?=
RDSSHD_HOST_KEY?=
RDSSHD_CONSOLE_LOCK?=	no
RDSSHD_KERNEL_STACK_PROTECTOR?=	yes
RDSSHD_PORT?=	22
RDSSHD_AI_IF?=	em0
RDSSHD_AI_HOSTNAME?=	rdinstall
RDSSHD_AI_IPV4?=	autoconf
RDSSHD_AI_NETMASK?=	255.255.255.0
RDSSHD_AI_ROUTE?=	none
RDSSHD_AI_IPV6?=	none
RDSSHD_AI_DOMAIN?=	my.domain
RDSSHD_AI_DNS?=	none
RDSSHD_FSSIZE?=	32768
RDSSHD_MRMAKEFSARGS?=	-s 20m \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096

rdsshd-check:
	@if [ -z "${RDSSHD_AUTHORIZED_KEYS}" ]; then \
		echo "set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2; \
		exit 1; \
	fi
	@case "${RDSSHD_CONSOLE_LOCK:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_CONSOLE_LOCK must be yes or no" >&2; exit 1;; \
	esac
	@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2; exit 1;; \
	esac

USE

Prepare a non-interactive next boot from the running system. No boot prompt or console access is assumed.

Whole-device image

minirootXX_sshd.img is a complete disk image. Writing it to a block device replaces the device's partition table and filesystems. Verify the output device before writing it.

Write the miniroot from Linuxraw
# Linux example. This destroys the existing contents of /dev/sda.
dd if=./miniroot79_sshd.img of=/dev/sda bs=512
sync
reboot

The running OS or storage stack may deny raw writes to the initial sectors of the device backing its active root filesystem. Use the boot-loader method if this cannot be changed remotely.

The image boots in UEFI or BIOS/CSM mode with Secure Boot disabled. Firmware must already select the target device. The rdroot runs from memory, so the installer can reuse that device as its target.

GRUB one-shot boot

Copy the uncompressed ramdisk kernel. Do not rely on gzio.

Copy ramdisk kernelraw
# Use the uncompressed kernel; do not rely on gzio.
cp /path/to/bsdsshd.rd /boot/bsdsshd.rd
/etc/grub.d/40_customraw
menuentry "OpenBSD bsdsshd.rd" {
        insmod part_gpt
        insmod ext2
        insmod bsd
        # Replace UUID with the /boot filesystem UUID.
        search --no-floppy --fs-uuid --set=root UUID
        kopenbsd /bsdsshd.rd
}
/etc/default/grubraw
GRUB_DEFAULT=saved
Select one-shot bootraw
update-grub
grub-reboot "OpenBSD bsdsshd.rd"
grub-editenv list

# Expected output:
# next_entry=OpenBSD bsdsshd.rd

sync
reboot

This example was tested in BIOS/CSM mode. UEFI is untested and may lack display console output. Secure Boot is unsupported. part_gpt and ext2 match the tested /boot filesystem. The target layout may require different modules. The GRUB build must provide the bsd module and kopenbsd command. Other boot loaders are untested.

Network booting

The matching OpenBSD/amd64 pxeboot(8) can load the compressed bsdsshd.rd over the network in place of bsd.rd.

Remote session

After the enhanced kernel boots, connect as root when the configured address accepts ssh. Perform required storage preparation, then start the interactive installer.

Remote installraw
ssh -i /path/to/private_key root@host.example
# Perform the required storage setup.
# Enter "done" at the network prompt to preserve network configuration.
install

RDSSHD_CONSOLE_LOCK=yes presents no local installer menu or shell.

REMOVE PATCH

Prepare private state before reversal. The target prints the recorded object path and removes the source obj symlink. /usr/obj is the default; use the printed path when different. patch(1) removes added files but leaves their empty parent directory. rmdir refuses non-empty directories.

Clean and reverse patchraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd prepare-unpatch-rdsshd

cd /usr/src
patch -R -p1 < /root/bsdsshd.rd.patch
# Replace /usr/obj below if prepare-unpatch-rdsshd printed another path.
rmdir /usr/obj/distrib/amd64/ramdisk_cd/rdsshd
rmdir /usr/src/distrib/amd64/ramdisk_cd/rdsshd

FILES

/usr/src/distrib/amd64/ramdisk_cd/rdsshd
Added source directory and wrapper entry point.
obj/bsdsshd.rd
Uncompressed enhanced ramdisk kernel.
obj/bsdsshd.gz
Stripped and compressed kernel installed as bsdsshd.rd by the release target and as /bsd in the enhanced miniroot.
obj/minirootXX_sshd.img
Enhanced BIOS- and EFI-bootable miniroot image.
obj/vnd
Private vnd ownership record. Removed after successful assembly or safe cleanup.

DIAGNOSTICS

private object directory is not active
Run the wrapper obj target as a separate invocation.
normal OpenBSD object root does not exist
Create BSDOBJDIR with normal OpenBSD ownership and permissions.
normal ramdisk_cd object directory is not active
The stock obj target did not select an object directory distinct from its source directory. Check BSDOBJDIR and rerun the build.
stock ramdisk_cd instbin was not built
Inspect the preceding stock-target failure. Clean ramdisk_cd before retrying after a failed trace link.
invalid public key on line N of RDSSHD_AUTHORIZED_KEYS
Replace the indicated line with a key accepted by ssh-keygen(1).
RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key
Supply an Ed25519 host private key without a passphrase.
stale private vnd state
Run unconfig-rdsshd before rebuilding the miniroot.
refusing to detach vnd not owned by rdsshd
The recorded device no longer covers the enhanced image. The target leaves it attached for manual inspection.

SEE ALSO

make(1), patch(1), ssh(1), ssh-keygen(1), softraid(4), vnd(4), disktab(5), install.site(5), sshd_config(5), autoinstall(8), bioctl(8), boot(8), boot_amd64(8), config(8), installboot(8), makefs(8), rdsetroot(8), release(8), sshd(8), and vnconfig(8).

various config files

config files

A place for config files to hangout and configure things

ngIRCd

An OpenBSD chroot orientated config for the awesome ngIRCd IRC daemon, likely useful on other platforms.

Note that there's a small DoS issue in the packaged version of ngIRCd for OpenBSD 7.9, so you might want to build a newer version from OpenBSD ports.

There also seems to be some great work being done on pledge/unveil integration upstream.

ngircd.confChrooted ngIRCd config for OpenBSDrawโ†“
# Hardened for the OpenBSD net/ngircd package (ngIRCd 27).
#
# When ChrootDir is enabled, prefer "rcctl restart ngircd" over reload.
# A reload is safe only when the same configuration is readable at
# /var/ngircd/etc/ngircd/ngircd.conf from inside the jail.

[Global]
# Placeholder: Name = <IRC_SERVER_FQDN>
# Example: irc.example.com
Name = irc.arachnogoat.com

# Placeholder: Info = <SERVER_DESCRIPTION>
# Example: Example private IRC server
Info = arachnogoat IRC server

# Placeholder: Network = <IRC_NETWORK_NAME> (ASCII without spaces)
# Example: ExampleIRC
Network = ArachnoIRC

# Placeholder: Password = <64_HEX_RANDOM_SECRET>
# Generate one with: openssl rand -hex 32
# Configure this as the IRC server password in every authorized client.
# This shared password controls admission to the whole server; ngIRCd 27's
# OpenBSD package does not provide a built-in per-user account database.
Password = example_password

# Disable the default unencrypted listener on port 6667.
Ports =

# OpenBSD rc.d does not require a PID file. Leaving this empty also lets the
# OpenBSD pledge(2) patch omit its cpath promise and keeps the jail read-only.
PidFile =

ServerUID = _ngircd
ServerGID = _ngircd

# Placeholder: MotdPhrase = <WELCOME_MESSAGE> (maximum 126 characters)
# Example: Welcome to the example IRC server.
MotdPhrase = Welcome to the arachnogoat IRC server.

# Placeholder: Listen = <PUBLIC_IPV4>,<PUBLIC_IPV6>
# Example using RFC documentation addresses (do not deploy these addresses):
# Listen = 192.0.2.10,2001:db8::10
# If omitted, ngIRCd listens on every local IPv4 and IPv6 address.
Listen = 51.83.6.53

# These limits are intentionally low
[Limits]
MaxConnections = 10
MaxConnectionsIP = 5
MaxJoins = 1
MaxListSize = 1

[Options]
# Placeholder: ChrootDir = <CHROOT_DIRECTORY>
# OpenBSD package example: /var/ngircd
ChrootDir = /var/ngircd

# Only the predefined channel below may exist.
AllowedChannelTypes =

AllowRemoteOper = no
DNS = no
Ident = no
DefaultUserModes = iI
MorePrivacy = yes
ScrubCTCP = yes

# Hide client IP addresses, user names, and real names from other clients.
# The cloak hash salt is intentionally generated afresh at each server start.
# Placeholder: CloakHost = hidden-%x.<CLOAK_SUFFIX>
# Example: hidden-%x.example-irc
CloakHost = hidden-%x.arachnoirc
CloakUserToNick = yes

NoticeBeforeRegistration = no
OperCanUseMode = no
OperChanPAutoOp = no
OperServerMode = no
WebircPassword =
SyslogFacility = daemon

[SSL]
# Placeholder: CertFile = <FULL_CHAIN_CERTIFICATE_FILE>
# OpenBSD acme-client example: /etc/ssl/irc.example.com.fullchain.pem
CertFile = /etc/ssl/irc.arachnogoat.com.fullchain.pem

# Placeholder: KeyFile = <PRIVATE_KEY_FILE>
# OpenBSD acme-client example: /etc/ssl/private/irc.example.com.key
KeyFile = /etc/ssl/private/irc.arachnogoat.com.key

# TLS 1.3 plus forward-secret AEAD suites for TLS 1.2 (LibreSSL syntax).
CipherList = TLSv1.3:ECDHE+AESGCM:ECDHE+CHACHA20

# Placeholder: Ports = <TLS_PORT>
# Standard encrypted IRC example: Ports = 6697
Ports = 6697

[Channel]
# Placeholder: Name = <CHANNEL_NAME>, including its # prefix
# Example: #arachnochat
Name = #arachnochat

# Placeholder: Topic = <CHANNEL_TOPIC>
# Example: Private chat
Topic = Private chat
Modes = +sntz
Autojoin = yes

# Optional per-user channel keys (disabled)
# -----------------------------------------
#
# NOTE: This per-user example configuration is untested in my environment,
#       Fully test and verify if you intend to follow these instructions.
#
# This is only a limited approximation of user accounts. Nicknames are not
# registered identities, and every client must still supply the shared global
# Password above before registering with the server.
#
# To enable separate channel-entry keys for user1, user2, and user3:
#
# 1. Generate four independent secrets with "openssl rand -hex 32": one
#    undisclosed master channel key plus one key for each user.
# 2. Replace the active channel settings above with:
#
#      Modes = +sntzNk <RANDOM_MASTER_CHANNEL_KEY>
#      Autojoin = no
#      KeyFile = /etc/ngircd/arachnochat.keys
#
#    Mode +k enables channel-key checking, and +N prevents nickname changes
#    while a user is in the channel. Autojoin must be disabled because it
#    cannot provide an individual channel key.
# 3. Create key file inside the chroot:
#
#      /var/ngircd/etc/ngircd/arachnochat.keys
#
#    Its format is "user:nick:key", without spaces. Because this configuration
#    uses "CloakUserToNick = yes", match any user name and the exact nickname:
#
#      *:user1:<RANDOM_KEY_FOR_USER1>
#      *:user2:<RANDOM_KEY_FOR_USER2>
#      *:user3:<RANDOM_KEY_FOR_USER3>
#
# 4. Secure the file as root-owned, readable by _ngircd, and not writable by
#    the daemon:
#
#      chown root:_ngircd /var/ngircd/etc/ngircd/arachnochat.keys
#      chmod 0440 /var/ngircd/etc/ngircd/arachnochat.keys
#
# 5. Restart ngIRCd. Each client chooses its assigned nickname and joins with:
#
#      /JOIN #arachnochat <THE_USER'S_CHANNEL_KEY>
#
# Security limitation: channel members can retrieve the +k master channel key
# using MODE and could share it. This mechanism provides convenience and
# separately revocable user keys, but it is not a strong account/ACL system.

Samba

An OpenBSD (on FFS2 filesystem) optimised config for Samba built to work with MacOS/iPadOS/iOS and MS Windows clients.

smb.confSamba config for OpenBSDrawโ†“
# Samba 4.22 on OpenBSD/FFS2.
[global]
   server role = standalone server
   workgroup = WORKGROUP
   security = user
   passdb backend = tdbsam

   ntlm auth = ntlmv2-only
   map to guest = never
   guest ok = no

   usershare max shares = 0

   # Enumerate only shares accessible to the authenticated user.
   access based share enum = yes

   disable netbios = yes
   smb ports = 445
   server min protocol = SMB3_00
   server max protocol = SMB3_11

   # Per-share encryption; sign unencrypted traffic.
   server smb encrypt = desired
   server signing = mandatory

   unix charset = UTF-8
   case sensitive = auto

   # No experimental SMB3 POSIX extensions.
   smb3 unix extensions = no

   wide links = no
   getwd cache = yes

   # Parent permissions override creation masks.
   inherit permissions = yes

   delete readonly = yes

   dos filemode = yes
   dos filetimes = yes

   # Directory leases require userspace oplocks and imply strict rename.
   oplocks = yes
   level2 oplocks = yes
   kernel oplocks = no
   smb2 leases = yes
   smb3 directory leases = yes

   # POSIX lock interoperability; disables durable handles.
   kernel share modes = no
   posix locking = yes

   strict locking = auto

   aio read size = 1
   aio write size = 1

   # Honour flushes without fsync on every write.
   strict sync = yes
   sync always = no

   # Avoid FFS2 preallocation overhead.
   strict allocate = no

   # Unsupported on OpenBSD.
   server multi channel support = no
   use sendfile = no

   # OpenBSD mkdir workaround (Samba bug 15801).
   vfs mkdir use tmp name = no

   # No native FFS2 xattrs; preserve existing ADS/EA backends and paths.
   vfs objects = streams_depot xattr_tdb
   ea support = yes
   store dos attributes = yes
   streams_depot:directory = /storage/samba/.streams_depot
   xattr_tdb:file = /storage/var/db/samba/xattr.tdb

   # Retain displaced streams as lost-* instead of deleting.
   streams_depot:delete_lost = no

   multicast dns register = no

   load printers = no
   disable spoolss = yes
   printcap name = /dev/null

   logging = file
   log level = 0
   max log size = 1000


[share]
   path = /storage/samba/share
   read only = no

   valid users = fileshare
   force user = fileshare
   force group = smbusers

   server smb encrypt = desired


[work]
   # Note: This share optimised for Windows clients
   path = /storage/samba/work
   read only = no

   valid users = filework
   force user = filework
   force group = smbusers

   # Avoid depot/TDB overhead; no ADS or persistent DOS attributes.
   vfs objects =
   ea support = no
   store dos attributes = no

   # Do not map DOS flags onto Unix modes.
   map archive = no
   map hidden = no
   map system = no
   map readonly = no


[private]
   path = /storage/samba/private
   read only = no

   valid users = fileprivate
   force user = fileprivate
   force group = smbusers

   server smb encrypt = required

various dotfiles

dotfiles

A place for dotfiles to hangout and do dotfile things

.vimrc

.vimrcAn uncomplicated .vimrcrawโ†“
" ~/.vimrc

" Syntax and filetype handling.
" Enable highlighting, indentation, and HTML-aware % matching.
syntax on
filetype plugin indent on
packadd! matchit

" Security.
" Ignore Vim settings embedded inside edited files.
set nomodeline

" Interface.
" Show useful position information with a little context around the cursor.
set number
set ruler
set laststatus=2
set scrolloff=3

" Searching.
" Search as you type, highlight matches, and ignore case unless capitals are used.
set incsearch
set hlsearch
set ignorecase
set smartcase
set showmatch

" Indentation.
" Use two-space soft tabs and insert spaces instead of literal tab characters.
set tabstop=2
set shiftwidth=2
set softtabstop=2
set expandtab

" Terminal.
" Disable the bell and assume a dark terminal background.
set visualbell
set t_vb=
set background=dark

various scripts

scripts

A place for scripts to hangout and script things

Blur optimised ffmpeg frame extractor

Who doesn't want rotating wallpaper sets extracted from their favourite videos!?

frame_extract.shA blur optimised frame to image extractorrawโ†“
#!/bin/sh

# Extracts the sharpest still frame near each regular time interval.
# Candidates within WINDOW are scored with FFmpeg blurdetect; the nearest is
# used if none has a finite score. Exact selected frames are written as lossless
# PNGs, optionally Lanczos-scaled to cover and centre-cropped to an exact size.
# No sharpening, frame interpolation, or deinterlacing; existing paths are not overwritten.

set -eu

LC_ALL=C
export LC_ALL

usage() {
    printf 'Usage: %s [-w WINDOW] [-o OUTDIR] [-s WIDTHxHEIGHT] INTERVAL INPUT\n' "$0" >&2
    exit 2
}

window=0.25
out=frames
size=
size_set=0

while getopts 'w:o:s:' opt; do
    case $opt in
        w) window=$OPTARG ;;
        o) out=$OPTARG ;;
        s) size=$OPTARG; size_set=1 ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))

[ "$#" -eq 2 ] || usage

interval=$1
input=$2

if [ "$size_set" -eq 1 ]; then
    case $size in
        *[!0-9x]* | *x*x* | x* | *x) usage ;;
    esac

    width=${size%%x*}
    height=${size#*x}

    case $width in *[1-9]*) ;; *) usage ;; esac
    case $height in *[1-9]*) ;; *) usage ;; esac
else
    width=
    height=
fi

awk -v i="$interval" -v w="$window" '
    BEGIN {
        re = "^[0-9]+([.][0-9]+)?$"
        exit !(i ~ re && w ~ re && i > 0 && w >= 0 && w < i / 2)
    }
' || {
    printf '%s\n' \
        "INTERVAL must be > 0; WINDOW must be >= 0 and < INTERVAL/2" >&2
    exit 2
}

command -v ffmpeg >/dev/null 2>&1 || {
    printf '%s\n' "ffmpeg not found" >&2
    exit 127
}

# Force relative pathnames to be treated as local files, not FFmpeg URLs.
case $input in /*) ;; *) input=./$input ;; esac
case $out   in /*) ;; *) out=./$out ;; esac

[ -f "$input" ] || {
    printf 'Input is not a regular file: %s\n' "$input" >&2
    exit 1
}

[ ! -e "$out" ] && [ ! -L "$out" ] || {
    printf 'Output path already exists: %s\n' "$out" >&2
    exit 1
}

tmpbase=${TMPDIR:-/tmp}
case $tmpbase in /*) ;; *) tmpbase=./$tmpbase ;; esac

tmp=$(mktemp -d "$tmpbase/bestframes.XXXXXX") || {
    printf '%s\n' "Could not create temporary directory" >&2
    exit 1
}

[ -n "$tmp" ] && [ -d "$tmp" ] || {
    printf '%s\n' "mktemp did not create a temporary directory" >&2
    exit 1
}

cleanup() {
    rm -f "$tmp/meta" "$tmp/picks" "$tmp/filter" 2>/dev/null || :
    rmdir "$tmp" 2>/dev/null || :
}
trap cleanup 0
trap 'exit 1' HUP INT TERM

printf '[1/2] Analysing candidate frames...\n' >&2

ffmpeg -nostdin -hide_banner -loglevel error \
    -stats -stats_period 2 \
    -i "$input" \
    -map 0:v:0 \
    -vf "setpts=PTS-STARTPTS,select='lte(abs(t-round(t/$interval)*$interval)\,$window)',blurdetect=block_width=32:block_height=32:block_pct=80,metadata=mode=print:key=lavfi.blur:file=-" \
    -f null /dev/null \
    >"$tmp/meta"

awk -v interval="$interval" '
    /^frame:/ {
        pts = time = ""

        for (i = 1; i <= NF; i++) {
            if ($i ~ /^pts:/) {
                pts = $i
                sub(/^pts:/, "", pts)
            } else if ($i ~ /^pts_time:/) {
                time = $i
                sub(/^pts_time:/, "", time)
            }
        }
        next
    }

    /^lavfi\.blur=/ && pts != "" && time != "" {
        blur = $0
        sub(/^[^=]*=/, "", blur)

        b = int(time / interval + 0.5)
        dist = time - b * interval
        if (dist < 0)
            dist = -dist

        # Fallback: nearest candidate if no finite blur score is available.
        if (!(b in nearpts) || dist < neardist[b]) {
            nearpts[b] = pts
            neardist[b] = dist
        }

        if (blur ~ /^[+-]?(([0-9]+([.][0-9]*)?)|([.][0-9]+))([eE][+-]?[0-9]+)?$/) {
            score = blur + 0

            if (!(b in bestpts) ||
                score < bestblur[b] ||
                (score == bestblur[b] && dist < bestdist[b])) {
                bestpts[b] = pts
                bestblur[b] = score
                bestdist[b] = dist
            }
        }
    }

    END {
        for (b in nearpts) {
            if (b in bestpts)
                print bestpts[b]
            else
                print nearpts[b]
        }
    }
' "$tmp/meta" >"$tmp/picks"

count=$(awk 'END { print NR + 0 }' "$tmp/picks")

[ "$count" -gt 0 ] || {
    printf '%s\n' "No suitable frames found" >&2
    exit 1
}

printf '[1/2] Analysis complete: %s frames selected.\n' "$count" >&2

# Build a balanced expression tree. This avoids FFmpeg parser-depth
# problems from a long flat eq()+eq()+eq()+... expression. If requested,
# scale to cover the target with square pixels, then crop exactly at centre.
awk -v width="$width" -v height="$height" '
    {
        a[NR] = "eq(pts\\," $1 ")"
    }

    END {
        n = NR

        while (n > 1) {
            m = 0

            for (i = 1; i <= n; i += 2) {
                m++

                if (i < n)
                    b[m] = "(" a[i] "+" a[i + 1] ")"
                else
                    b[m] = a[i]
            }

            for (i = 1; i <= m; i++)
                a[i] = b[i]

            n = m
        }

        filter = "setpts=PTS-STARTPTS,select=" a[1]

        if (width != "")
            filter = filter ",scale=w=" width ":h=" height \
                ":force_original_aspect_ratio=increase" \
                ":flags=lanczos+accurate_rnd+full_chroma_int:reset_sar=1" \
                ",crop=w=" width ":h=" height ":exact=1"

        print filter
    }
' "$tmp/picks" >"$tmp/filter"

mkdir "$out" || {
    printf 'Could not create output directory: %s\n' "$out" >&2
    exit 1
}

printf '[2/2] Extracting %s lossless PNG frames...\n' "$count" >&2

if ! ffmpeg -nostdin -hide_banner -loglevel error \
    -stats -stats_period 2 -n \
    -i "$input" \
    -map 0:v:0 \
    -/filter:v "$tmp/filter" \
    -fps_mode passthrough \
    -start_number 0 \
    "$out/frame_%06d.png"
then
    printf 'Extraction failed; partial output may remain in: %s\n' "$out" >&2
    exit 1
fi

actual=0
for file in "$out"/frame_*.png; do
    [ -f "$file" ] || continue
    actual=$((actual + 1))
done

[ "$actual" -eq "$count" ] || {
    printf 'Expected %s frames but wrote %s; output left in: %s\n' \
        "$count" "$actual" "$out" >&2
    exit 1
}

printf 'Done: %s frames written to %s/\n' "$count" "$out" >&2

ffmpeg parallel AAC-LC converter

The Malazan audiobooks are too huge for Rockbox when using AAC-HE encoding

m4b2aac.shParallel ffmpeg AAC-LC conversionrawโ†“
#!/bin/sh
# Convert M4B/M4A files to AAC-LC, preserving tags, chapters and artwork.
# -j count: concurrent conversions (default 2).
# -b kbps: total audio bitrate, a positive integer in kb/s (default 96).
# -c channels: 1 for mono, 2 for stereo (default 2).
# -o file: exact output path, for one input only; use .m4b or .m4a.
#    cannot be combined with -d or -s. Output directories must already exist.
# -d directory: existing output directory; keep input stems and extension type.
# -s suffix: append this to generated stems (default .aac-lc, or empty with -d).
# -o cannot be combined with -d or -s. Output directories must already exist.
#
# One file, several files, or several patterns:
#   ./m4b2aac.sh -- './Book One.m4b'
#   ./m4b2aac.sh -j 4 -- './Book One.m4b' './Book Two.m4a'
#   ./m4b2aac.sh -j 4 -- ./originals/*.m4b ./more/*.m4a
#
# Exact output name, custom suffix, or a separate output directory:
#   ./m4b2aac.sh -o './Book LC.m4b' -- './Book.m4b'
#   ./m4b2aac.sh -s '.rockbox' -- ./originals/*.m4b
#   mkdir -p ./m4b-aac-lc &&
#   ./m4b2aac.sh -d ./m4b-aac-lc -j 4 -- ./*.m4b
#   ./m4b2aac.sh -d ./m4b-aac-lc -s '.lc' -- './Book.m4b'
#
# Manual batch changing .m4a to .m4b; stop at the first error:
#   mkdir -p ./converted &&
#   for file in ./originals/*.m4a; do
#       name=${file##*/}
#       ./m4b2aac.sh -o "./converted/${name%.*}.m4b" -- "$file" || break
#   done
#
# Recursive conversion beside originals; exclude staging and generated files:
#   find ./books -type d -name '.m4b2aac.*' -prune -o \
#       -type f -name '*.[mM]4[aAbB]' ! -name '*.aac-lc.m4[ab]' \
#       -exec /absolute/path/m4b2aac.sh -j 4 -- {} +

usage() {
    printf 'Usage: %s [-j jobs] [-b kbps] [-c 1|2] [-d directory] [-s suffix] [--] file ...\n       %s [-j jobs] [-b kbps] [-c 1|2] -o output [--] file\n' "$0" "$0" >&2
    exit 2
}
jobs=2
bitrate=96
channels=2
output_file=
target_dir=
suffix=.aac-lc
suffix_set=
while getopts 'j:b:c:o:d:s:' option; do
    case $option in
        j) jobs=$OPTARG ;;
        b) bitrate=$OPTARG ;;
        c) channels=$OPTARG ;;
        o) output_file=$OPTARG; [ -n "$output_file" ] || usage ;;
        d) target_dir=$OPTARG; [ -n "$target_dir" ] || usage ;;
        s) suffix=$OPTARG; suffix_set=1 ;;
        *) usage ;;
    esac
done
shift "$((OPTIND - 1))"
for count in "$jobs" "$bitrate"; do
    case $count in ''|*[!0-9]*) usage ;; esac
    [ "$count" -gt 0 ] 2>/dev/null || usage
done
case $channels in 1|2) ;; *) usage ;; esac
[ "$#" -gt 0 ] || usage
case $suffix in */*) usage ;; esac
if [ -n "$output_file" ]; then
    [ "$#" -eq 1 ] && [ -z "$target_dir$suffix_set" ] || usage
    case $output_file in *.[mM]4[aAbB]) ;; *) usage ;; esac
    case $output_file in /*|./*|../*) ;; *) output_file=./$output_file ;; esac
fi
if [ -n "$target_dir" ]; then
    case $target_dir in /*|./*|../*) ;; *) target_dir=./$target_dir ;; esac
    [ -d "$target_dir" ] || {
        printf 'Not a directory: %s\n' "$target_dir" >&2; exit 1
    }
    [ -n "$suffix_set" ] || suffix=
fi
for tool in ffmpeg mktemp; do
    command -v "$tool" >/dev/null 2>&1 || {
        printf 'Missing command: %s\n' "$tool" >&2; exit 1
    }
done

paths() {
    # Prefix bare paths so FFmpeg and POSIX utilities cannot read them as options.
    case $1 in /*|./*|../*) file=$1 ;; *) file=./$1 ;; esac
    name=${file##*/}
    case $name in *.[mM]4[aA]) extension=m4a ;; *) extension=m4b ;; esac
    directory=${target_dir:-${file%/*}}
    output=${output_file:-${directory%/}/${name%.*}$suffix.$extension}
    directory=${output%/*}
    [ -n "$directory" ] || directory=/
    name=${output##*/}
}

work=$(mktemp -d "${TMPDIR:-/tmp}/m4b2aac.XXXXXX") || exit 1
token=.${work##*/}
pending=
status=0
polls=0
# shellcheck disable=SC2329 # Called by the exit trap.
cleanup() {
    trap '' HUP INT TERM
    # Entries contain only a child PID and a numeric job ID.
    for job in $pending; do
        [ -f "$work/${job#*:}.done" ] || kill -s TERM "${job%:*}" 2>/dev/null || :
    done
    for job in $pending; do wait "${job%:*}" 2>/dev/null || :; done
    for record in "$work"/*.directory; do
        [ -f "$record" ] || continue
        stage=$(cat "$record") || continue
        case $stage in */"$token") rm -rf "$stage" ;; esac
    done
    rm -rf "$work"
}
trap cleanup 0
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

# Reserve names on each destination filesystem before starting any encoders.
# A shared random name also recognises directory aliases without parsing paths.
printf '%s\n' "$work" >"$work/owner" || exit 1
id=0
for file do
    id=$((id + 1))
    case $file in *.[mM]4[aAbB]) ;; *) continue ;; esac
    [ -f "$file" ] || continue
    paths "$file"
    [ -e "$output" ] || [ -L "$output" ] || {
        stage=$directory/$token
        if mkdir -m 700 "$stage" 2>/dev/null; then
            printf '%s' "$stage" >"$work/$id.directory" || {
                rm -f "$work/$id.directory"
                rmdir "$stage"; exit 1
            }
            cp "$work/owner" "$stage/owner" && mkdir "$stage/names" || exit 1
        elif ! cmp -s "$work/owner" "$stage/owner"; then
            printf 'Cannot stage output in: %s\n' "$directory" >&2; exit 1
        fi
        mkdir "$stage/names/$name" 2>/dev/null || {
            printf 'Conflicting or invalid output filename: %s\n' "$output" >&2; exit 1
        }
    }
done

# The background invocation supplies the subshell; avoid another process layer.
convert() {
    id=$1
    paths "$2"
    encoder=
    # shellcheck disable=SC2329 # Called by the exit trap.
    finish() {
        trap '' HUP INT TERM
        if [ -n "$encoder" ]; then
            kill -s TERM "$encoder" 2>/dev/null || :
            wait "$encoder" 2>/dev/null || :
        fi
        : >"$work/$id.done"
    }
    trap finish 0
    trap 'exit 129' HUP
    trap 'exit 130' INT
    trap 'exit 143' TERM

    case $file in
        *.[mM]4[aAbB]) ;;
        *) printf 'Expected an M4B/M4A file: %s\n' "$file" >&2; exit 1 ;;
    esac
    [ -f "$file" ] || {
        printf 'Not a regular file: %s\n' "$file" >&2; exit 1
    }
    if [ -e "$output" ] || [ -L "$output" ]; then
        printf 'Exists, skipping: %s\n' "$output" >&2; exit 0
    fi
    tmp=$directory/$token/$id
    mkdir "$tmp" || exit 1
    printf '[%s] Started: %s\n' "$id" "$file" >&2
    # Separate inputs avoid FFmpeg 9 truncating these books when copying artwork.
    # Millisecond movie/chapter timing avoids overflow with long chapter gaps.
    ffmpeg -hide_banner -nostdin -nostats -v error -xerror -n \
        -stats_period 15 -progress "$work/$id.progress" \
        -i "$file" -i "$file" -map 0:a:0 -map '1:v:disp:attached_pic?' \
        -af 'aresample=async=1:first_pts=0' \
        -c:a aac -profile:a aac_low -ar 24000 \
        -b:a "${bitrate}k" -ac "$channels" -c:v copy -movie_timescale 1000 \
        "$tmp/$name" >"$tmp/log" 2>&1 &
    encoder=$!
    result=0
    wait "$encoder" || result=$?
    encoder=
    if [ "$result" -ne 0 ] || [ ! -s "$tmp/$name" ]; then
        printf '[%s] Failed: %s\n' "$id" "$file" >&2
        cat "$tmp/log" >&2
        exit 1
    fi
    # EOF declines overwrite if a destination appeared during encoding.
    mv -i "$tmp/$name" "$directory/" </dev/null &&
        [ ! -f "$tmp/$name" ] || {
        printf '[%s] Cannot publish: %s\n' "$id" "$output" >&2; exit 1
    }
    printf '[%s] Created: %s\n' "$id" "$output"
}

reap() {
    remaining=
    active=0
    polls=$(((polls + 1) % 15))
    for job in $pending; do
        if [ -f "$work/${job#*:}.done" ] || ! kill -s 0 "${job%:*}" 2>/dev/null; then
            wait "${job%:*}" || status=1
        else
            remaining="$remaining $job"
            active=$((active + 1))
            if [ "$polls" -eq 0 ] && [ -f "$work/${job#*:}.progress" ]; then
                elapsed=$(sed -n 's/^out_time=//p' "$work/${job#*:}.progress" | tail -n 1)
                [ -z "$elapsed" ] ||
                    printf '[%s] Encoded: %s\n' "${job#*:}" "${elapsed%.*}" >&2
            fi
        fi
    done
    pending=$remaining
}
id=0
active=0
for file do
    while [ "$active" -ge "$jobs" ]; do
        reap
        [ "$active" -lt "$jobs" ] || sleep 1
    done
    id=$((id + 1))
    convert "$id" "$file" &
    pending="$pending $!:$id"
    active=$((active + 1))
done
while [ -n "$pending" ]; do
    reap
    [ -z "$pending" ] || sleep 1
done
exit "$status"

ffprobe CUE file generator

The Malazan audiobooks can have proper chapter navigation and title text

m4b2cue.shffprobe CUE file generatorrawโ†“
#!/bin/sh
# Create a Rockbox cuesheet beside each M4B/M4A file operand.
# Book.m4b or Book.m4a -> Book.cue in the same directory; audio is unchanged.
# Existing CUEs are skipped. Options go before inputs; -- ends option parsing.
#
# Single, multiple, and multiple glob patterns:
#   ./m4b2cue.sh -- './Book One.m4b'
#   ./m4b2cue.sh -- './Book One.m4b' '../Book Two.m4a'
#   ./m4b2cue.sh -- ./converted/*.m4b ./other/*.m4a
#   find ./books -type f -name '*.[mM]4[aAbB]' \
#       -exec /absolute/path/m4b2cue.sh -- {} +
#
# -m supplies one book's mapping: first source track number, then title.
# Numbers are 1-based indexes into that recording's extracted sections.
# -m requires exactly one audio input, even if a glob supplied the input.
# Mapping filenames need not match the audio name:
#   ./m4b2cue.sh -m './Book.chapters.txt' -- './Book.aac-lc.m4b'
#
# Manual batch: originals and mappings here, converted files in ./m4b-aac-lc.
# Stop at the first error:
#   for file in ./*.m4b; do
#       ./m4b2cue.sh -m "${file%.m4b}.chapters.txt" \
#           -- "./m4b-aac-lc/${file#./}" || break
#   done
#
# Recursive mapping when each input has a matching Book.chapters.txt beside it:
#   find ./books -type f -name '*.[mM]4[aAbB]' -exec sh -c '
#       /absolute/path/m4b2cue.sh -m "${1%.*}.chapters.txt" -- "$1"
#   ' sh {} \;

usage() {
    printf 'Usage: %s [--] file ...\n       %s -m mapping [--] file\n' "$0" "$0" >&2
    exit 2
}
mapping=
while getopts 'm:' option; do
    case $option in
        m) mapping=$OPTARG; [ -n "$mapping" ] || usage ;;
        *) usage ;;
    esac
done
shift "$((OPTIND - 1))"
[ "$#" -gt 0 ] || usage
if [ -n "$mapping" ]; then
    [ "$#" -eq 1 ] || usage
    [ -f "$mapping" ] || {
        printf 'Not a regular mapping file: %s\n' "$mapping" >&2
        exit 1
    }
fi
for tool in ffprobe jq mktemp; do
    command -v "$tool" >/dev/null 2>&1 || {
        printf 'Missing command: %s\n' "$tool" >&2
        exit 1
    }
done

LC_ALL=C
export LC_ALL
trap 'exit 1' HUP INT TERM

convert() (
    case $1 in /*|./*|../*) file=$1 ;; *) file=./$1 ;; esac
    case $file in
        *.[mM]4[aAbB]) ;;
        *) printf 'Expected an M4B/M4A file: %s\n' "$file" >&2; exit 1 ;;
    esac
    [ -f "$file" ] || {
        printf 'Not a regular file: %s\n' "$file" >&2
        exit 1
    }
    directory=${file%/*}/
    cue=${file%.*}.cue
    # Rockbox associates both Book.m4b and Book.m4a with Book.cue.
    set -- "${file%.*}".[mM]4[aAbB]
    if [ "$#" -gt 1 ]; then
        printf 'Conflicting audio filenames: %s\n' "$file" >&2
        exit 1
    fi
    if [ -e "$cue" ] || [ -L "$cue" ]; then
        printf 'Exists, skipping: %s\n' "$cue" >&2
        exit 0
    fi
    tmp=$(mktemp -d "${directory}.m4b2cue.XXXXXX") || exit 1
    staged=$tmp/${cue##*/}
    trap 'rm -f "$tmp/metadata" "$staged"; rmdir "$tmp"' 0
    trap 'exit 1' HUP INT TERM
    # Separate commands let POSIX sh check both exits without pipefail.
    ffprobe -v error -of json \
        -show_entries 'chapter=start_time:chapter_tags:format_tags' \
        "$file" >"$tmp/metadata" &&
    jq -er --arg file "${file##*/}" --arg mapping_path "$mapping" \
        --rawfile mapping "${mapping:-/dev/null}" '
        def tags: .tags // {} | with_entries(.key |= ascii_downcase)
            | with_entries(select(.value != ""));
        def text:
            .[:240] | gsub("[\u0000-\u001f\u007f-\u009f]"; " ")
            | gsub("\""; "\u0027")
            | until(utf8bytelength <= 240; .[:-1]);
        def pad: tostring | if length < 2 then "0" + . else . end;
        def index:
            [(./4500 | floor | pad), (./75 | floor % 60 | pad),
             (. % 75 | pad)] | join(":");

        if ($file | test("[\"\u0000-\u001f\u007f-\u009f\ufffd]"))
            or ($file | utf8bytelength > 247) then
            error("filename cannot be represented safely in a Rockbox cuesheet")
        else . end
        | if $mapping_path != "" then
            (.chapters // []) as $tracks
            | [$mapping | ltrimstr("\uFEFF") | split("\n")[]
                | gsub("^[ \t]+|[ \t\r]+$"; "") | select(length > 0)
                | (capture("^(?<track>[0-9]+)[ \t]+(?<title>.+)$")
                    // error("mapping lines must contain a track number and title"))
                | .track |= tonumber] as $groups
            | if ($groups | length) == 0 or $groups[0].track != 1
                or any($groups[]; .track > ($tracks | length))
                or any(range(1; $groups | length);
                       $groups[.].track <= $groups[. - 1].track) then
                error("mapping must start at track 1 and increase within the source track count")
              else .chapters = [$groups[] |
                $tracks[.track - 1] + {tags: {title: .title}}] end
          else . end
        | (.chapters // []) as $chapters
        # This parser stops at TRACK 99, before reading its TITLE/INDEX.
        | if ($chapters | length) == 0 then error("no chapters")
          elif ($chapters | length) > 98 then
            error("this Rockbox parser supports only 98 complete chapters")
          else . end
        | [$chapters[] | .start_time | tonumber
            | if isfinite and . >= 0 and . < 1800060 then
                . * 75 | floor
              else error("chapter start is outside Rockbox limits") end] as $frames
        | if any(range(1; $frames | length);
                 $frames[.] <= $frames[. - 1]) then
            error("chapter starts must increase at 1/75-second resolution")
          else . end
        | (.format | tags) as $tags
        | "\uFEFFTITLE \"\(($tags.title // $tags.album // $file) | text)\"",
          "PERFORMER \"\(($tags.artist // $tags.album_artist // "") | text)\"",
          "FILE \"\($file)\" MP3",
          ($chapters | to_entries[] | .key as $i |
            "  TRACK \($i + 1 | pad) AUDIO",
            "    TITLE \"\((.value | tags | .title // "Chapter \($i + 1)") | text)\"",
            "    INDEX 01 \($frames[$i] | index)")
    ' "$tmp/metadata" >"$staged" &&
    # -i with EOF declines replacement if a cue appeared during probing.
    # Target the parent directory so a new directory at $cue is an error.
    mv -i "$staged" "$directory" </dev/null && [ ! -f "$staged" ] &&
    printf 'Created: %s\n' "$cue"
)

status=0
for file do
    convert "$file" || {
        printf 'Failed: %s\n' "$file" >&2
        status=1
    }
done
exit "$status"

AlmaLinux guide

IRC with Irssi and tmux

Keep a persistent Irssi connection available across SSH sessions.

DESCRIPTION

Irssi provides the IRC client. tmux keeps the client running after an SSH connection closes and allows the same terminal session to be resumed later.

The examples connect securely to irc.arachnogoat.com on port 6697 and join #chat. Replace YourNick and SERVER_PASSWORD before use.

INSTALLATION

Install tmux and Irssi from the AlmaLinux package repositories. Run the command from a normal shell with an account permitted to use sudo.

Install tmux and IrssiRun from a shell on AlmaLinuxraw
sudo dnf install tmux irssi

START A SESSION

Create a tmux session named irc. The command enters that session immediately.

Start the IRC tmux sessionraw
tmux new -s irc

Run irssi inside the new tmux session. Irssi then occupies the tmux window until it exits or the session is detached.

CONNECT

Enter the following commands at the Irssi prompt. The connection uses TLS with certificate verification, then joins #chat.

Initial Irssi connectionReplace the nickname and server passwordraw
/SET nick YourNick
/CONNECT -tls -tls_verify irc.arachnogoat.com 6697 SERVER_PASSWORD
/JOIN #chat

NAVIGATION

Irssi normally places server status and the joined channel in separate windows. Use /WIN 1 for the first window and /WIN 2 for the second. The status bar shows the actual window numbers when they differ.

AUTOMATIC CONNECTION

Add a named network, its server, and the channel to Irssi, then save the configuration. Irssi will reconnect and join #chat on later starts.

Persistent Irssi configurationReplace SERVER_PASSWORD before useraw
/NETWORK ADD ArachnoIRC
/SERVER ADD -auto -tls -tls_verify -network ArachnoIRC irc.arachnogoat.com 6697 SERVER_PASSWORD
/CHANNEL ADD -auto #chat ArachnoIRC
/SAVE

/SAVE writes the supplied server password to the user's Irssi configuration. Protect that account and its ~/.irssi directory accordingly.

DETACH AND RETURN

Leave Irssi running

Press Ctrl-b, release both keys, then press d. tmux detaches while Irssi and its IRC connection continue running.

Resume the session

After reconnecting through SSH, attach to the existing tmux session from the shell.

Resume the IRC tmux sessionraw
tmux attach -t irc

SEE ALSO

See tmux(1) and the Irssi built-in /HELP command for additional session, window, and connection controls.

Message sent

X

Thank you. Your message has been sent.