hwupdate: PawletOS hardware/peripheral firmware update subsystem

Adds pawlethwd, a native C++ system_ext daemon (coredomain) that detects
peripherals, checks the hardware update catalog (ota.php?mode=hardware),
verifies payloads, and flashes firmware via authorized handler plugins.

- daemon/: manifest parse, libcurl HTTP + sha256, sysfs USB detection,
  catalog client, dlopen handler registry (fixed path + ABI + allowlist),
  state persistence, orchestration engine, AIDL service, main.
- aidl/: IPawletHardwareUpdate (getComponents/getPendingUpdates/checkNow/...).
- include/pawlet/hwh_abi.h: stable C plugin ABI.
- handlers/usbdfu/: libpawlethwh_usbdfu.so, DFU 1.1 download over libusb.
- sepolicy/: pawlethwd domain, pawlet_hw_data_file, service type.
- schemas/, etc/supported_hardware.default.json, README design doc.
- hwupdate.mk wires packages + ro.pawlet.hardware.manifest + sepolicy dir;
  inherited from config/common.mk.

Trust: a server-named installer handler is honored only if allowlisted in
the immutable supported_hardware.json, maps to a fixed dm-verity-protected
.so path, is ABI/id-checked, and every payload is sha256-verified before a
handler runs.
This commit is contained in:
oxmc
2026-07-18 01:11:15 -07:00
parent e58d2cf2ac
commit 4e9f147e55
39 changed files with 2175 additions and 0 deletions
+5
View File
@@ -111,6 +111,11 @@ BOARD_SEPOLICY_DIRS += packages/apps/BgUpd/sepolicy
# ---------------------------------------------------------------------------
$(call inherit-product, vendor/pawlet/config/microg.mk)
# ---------------------------------------------------------------------------
# Hardware update subsystem (pawlethwd) — peripheral firmware updates
# ---------------------------------------------------------------------------
$(call inherit-product, vendor/pawlet/hwupdate/hwupdate.mk)
# ---------------------------------------------------------------------------
# PawletOS build-time framework resource overlay (adds new drawables)
# ---------------------------------------------------------------------------
+19
View File
@@ -0,0 +1,19 @@
// Copyright (C) 2026 oxmc / PawletOS
// SPDX-License-Identifier: Apache-2.0
// Shared handler ABI header, used by the daemon and every handler plugin.
cc_library_headers {
name: "pawlet_hwh_headers",
export_include_dirs: ["include"],
}
// Default image-local hardware manifest. A device tree may install its own
// supported_hardware.json to override this. The schemas/ files are
// documentation/CI artifacts and are intentionally not shipped on-device.
prebuilt_etc {
name: "pawlet_supported_hardware_default",
src: "etc/supported_hardware.default.json",
filename: "supported_hardware.json",
sub_dir: "pawlet",
system_ext_specific: true,
}
+84
View File
@@ -0,0 +1,84 @@
<!--
Copyright (C) 2026 oxmc / PawletOS
SPDX-License-Identifier: Apache-2.0
-->
# PawletOS hardware update subsystem (`pawlethwd`)
Detects integrated and attached peripherals (displays, touch controllers, docks,
MCUs, HATs, automotive hardware, …) and updates their firmware over
device-specific transports (USB, i2c, spi, uart, gpio, can). This is separate
from the Android system OTA (`me.pawlet.updater`), which updates the OS image.
## Components
| Piece | What | Installs to |
|-------|------|-------------|
| `pawlethwd` | native C++ system daemon: detect → check → verify → flash → state | `/system_ext/bin/pawlethwd` |
| `IPawletHardwareUpdate` | AIDL: list components, trigger a check, read state | `pawlet_hwupdate` service |
| handler plugins | per-transport `.so` flashers, `dlopen`ed by the daemon | `/system_ext/lib{,64}/pawlet/hwh/libpawlethwh_<id>.so` |
| `supported_hardware.json` | image-local authoritative manifest (what this image supports) | `/system_ext/etc/pawlet/supported_hardware.json` |
Even though the source lives in the vendor repo, the daemon is a **system-side
(`system_ext`, `coredomain`)** component — it needs network (firmware download)
and registers a binder service, both of which a `/vendor` daemon cannot do
without violating AOSP neverallows.
## Trust model
The server catalog (`device-updates.json`) only *describes* releases; it never
selects executable code. Root of trust is the read-only, dm-verity-protected
`/system_ext` partition:
1. A server-named `installer.handler` is honored **only** if that handler id is
in the component's `installer_handlers` allowlist in the local, immutable
`supported_hardware.json`.
2. The allowlisted id maps to exactly
`/system_ext/lib64/pawlet/hwh/libpawlethwh_<id>.so`; the daemon refuses any
other path and refuses a plugin whose reported `abi_version`/`handler_id`
don't match.
3. Every payload is SHA-256 verified against the catalog before a handler runs.
So the server can only request handlers already baked into the signed image.
## Flow
```
boot / hotplug / AIDL checkNow
read supported_hardware.json ──▶ fetch catalog (ota.php?mode=hardware&target=catalog)
│ │
│ detect present hardware (USB VID/PID, DT compatible, …)
│ │
└── for each detected component ALSO supported by the local manifest:
read installed version (handler get_version)
query ota.php?mode=hardware&target=update&… ─▶ newest compatible or 204
download + sha256 verify
dlopen allowlisted handler .so → flash()
persist installed version / retry state
```
## Handler plugin ABI
See `include/pawlet/hwh_abi.h`. A handler is a `.so` exporting a single symbol
`pawlet_hwh_entry()` returning a `const pawlet_hwh_api*` vtable
(`abi_version`, `handler_id`, `detect`, `get_version`, `flash`). The USB DFU
handler (`handlers/usbdfu`, `libpawlethwh_usbdfu.so`) is the first
implementation.
## Layout
```
hwupdate/
aidl/ IPawletHardwareUpdate + parcelables
include/pawlet/ hwh_abi.h (shared by daemon + handlers)
daemon/ pawlethwd sources
handlers/usbdfu/ libpawlethwh_usbdfu.so
sepolicy/ pawlethwd domain
etc/ init.pawlethwd.rc, supported_hardware.default.json
schemas/ JSON schemas (CI validation)
hwupdate.mk product inclusion (prop + manifest + modules)
```
Per-device manifests may override the default by installing their own
`supported_hardware.json` from the device tree.
+19
View File
@@ -0,0 +1,19 @@
// Copyright (C) 2026 oxmc / PawletOS
// SPDX-License-Identifier: Apache-2.0
aidl_interface {
name: "me.pawlet.hardware.update",
unstable: true,
srcs: ["me/pawlet/hardware/update/*.aidl"],
backend: {
cpp: {
enabled: true,
},
ndk: {
enabled: true,
},
java: {
enabled: true,
},
},
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
package me.pawlet.hardware.update;
/** Status of a hardware component known to the image manifest. */
parcelable HwComponent {
/** Stable component id from supported_hardware.json. */
@utf8InCpp String componentId;
/** Owning product id. */
@utf8InCpp String productId;
/** Component class, e.g. "microcontroller", "touch-controller". */
@utf8InCpp String componentClass;
/** Role of this instance, e.g. "primary-touch" (may be empty). */
@utf8InCpp String role;
/** Transport, e.g. "usb", "i2c". */
@utf8InCpp String transport;
/** Whether the component was detected as present on the device. */
boolean present;
/** Installed firmware version if known (may be empty). */
@utf8InCpp String installedVersion;
/** Detected hardware revision if known (may be empty). */
@utf8InCpp String hardwareRevision;
/** Support level from the manifest: full/experimental/limited/deprecated. */
@utf8InCpp String support;
}
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
package me.pawlet.hardware.update;
/** A firmware update available (or last applied) for a component. */
parcelable HwUpdateInfo {
@utf8InCpp String componentId;
/** True if a newer compatible update is available for this component. */
boolean available;
/** Offered firmware version (may be empty when none available). */
@utf8InCpp String version;
/** Payload size in bytes. */
long sizeBytes;
/** Optional changelog URL. */
@utf8InCpp String changelogUrl;
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
package me.pawlet.hardware.update;
import me.pawlet.hardware.update.HwComponent;
import me.pawlet.hardware.update.HwUpdateInfo;
/**
* Binder interface to the PawletOS hardware update daemon (pawlethwd).
* Registered as "pawlet_hwupdate". Read/trigger only — the daemon performs all
* detection, download, verification, and flashing itself.
*/
interface IPawletHardwareUpdate {
/** Components known to the image manifest with current detection state. */
HwComponent[] getComponents();
/** Updates currently available for detected, supported components. */
HwUpdateInfo[] getPendingUpdates();
/**
* Trigger a detect + check (+ apply, per policy) cycle. Returns immediately;
* the cycle runs asynchronously. Use getLastResult()/getComponents() to
* observe progress.
*/
void checkNow();
/** Whether a check/apply cycle is currently running. */
boolean isBusy();
/** Result code of the last completed cycle (0 == success). */
int getLastResult();
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright (C) 2026 oxmc / PawletOS
// SPDX-License-Identifier: Apache-2.0
cc_binary {
name: "pawlethwd",
system_ext_specific: true,
srcs: ["*.cpp"],
local_include_dirs: ["."],
header_libs: ["pawlet_hwh_headers"],
shared_libs: [
"libbase",
"liblog",
"libbinder",
"libutils",
"libcutils",
"libcurl",
"libcrypto",
"libjsoncpp",
"me.pawlet.hardware.update-cpp",
],
cflags: [
"-Wall",
"-Werror",
"-Wextra",
],
init_rc: ["init.pawlethwd.rc"],
}
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "catalog.h"
#include <curl/curl.h>
#include <json/json.h>
#include <sstream>
#include <android-base/logging.h>
#include "http.h"
namespace pawlet::hwupdate {
namespace {
std::string Escape(CURL* c, const std::string& s) {
char* e = curl_easy_escape(c, s.c_str(), static_cast<int>(s.size()));
std::string out = e ? e : s;
if (e) curl_free(e);
return out;
}
std::string BuildUrl(const std::string& baseUrl, const CheckRequest& r) {
CURL* c = curl_easy_init();
std::ostringstream u;
u << baseUrl;
// baseUrl already contains "?mode=hardware"; append remaining params.
u << "&target=update";
u << "&device=" << Escape(c, r.device);
u << "&pawlet_version=" << Escape(c, r.pawletVersion);
u << "&hardware_id=" << Escape(c, r.hardwareId);
u << "&component_id=" << Escape(c, r.componentId);
if (!r.hardwareRevision.empty()) u << "&hardware_revision=" << Escape(c, r.hardwareRevision);
if (!r.currentVersion.empty()) u << "&current_version=" << Escape(c, r.currentVersion);
if (!r.bootloaderVersion.empty()) u << "&bootloader_version=" << Escape(c, r.bootloaderVersion);
if (c) curl_easy_cleanup(c);
return u.str();
}
std::optional<UpdateEntry> ParseUpdate(const std::string& body) {
Json::Value root;
Json::CharReaderBuilder b;
std::string errs;
std::istringstream in(body);
if (!Json::parseFromStream(b, in, &root, &errs)) {
LOG(WARNING) << "hwupdate: bad update response: " << errs;
return std::nullopt;
}
const Json::Value& u = root["update"];
if (!u.isObject()) return std::nullopt;
UpdateEntry e;
e.version = u.get("version", "").asString();
e.datetime = u.get("datetime", 0).asInt64();
e.filename = u.get("filename", "").asString();
e.id = u.get("id", "").asString();
e.sizeBytes = u.get("size_bytes", 0).asInt64();
e.url = u.get("url", "").asString();
e.checksumSha256 = u.get("checksum_sha256", "").asString();
e.changelogUrl = u.get("changelog_url", "").asString();
const Json::Value& inst = u["installer"];
e.installerType = inst.get("type", "").asString();
e.installerHandler = inst.get("handler", "").asString();
if (e.url.empty() || e.checksumSha256.empty() || e.installerHandler.empty()) {
LOG(WARNING) << "hwupdate: update entry missing required fields";
return std::nullopt;
}
return e;
}
} // namespace
std::optional<UpdateEntry> CheckUpdate(const std::string& baseUrl, const CheckRequest& req) {
std::string url = BuildUrl(baseUrl, req);
std::string body;
long code = 0;
if (!http::Get(url, &body, &code)) return std::nullopt;
if (code == 204) return std::nullopt; // up to date
if (code != 200) {
LOG(WARNING) << "hwupdate: update check http=" << code << " for " << req.componentId;
return std::nullopt;
}
return ParseUpdate(body);
}
} // namespace pawlet::hwupdate
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <optional>
#include <string>
#include "types.h"
namespace pawlet::hwupdate {
struct CheckRequest {
std::string device;
std::string pawletVersion;
std::string hardwareId;
std::string componentId;
std::string hardwareRevision;
std::string currentVersion;
std::string bootloaderVersion; // optional
};
// Query ota.php?mode=hardware&target=update for the newest compatible update.
// Returns the update on HTTP 200, nullopt on 204 (up to date) or any error. The
// server performs compatibility/version filtering; the daemon re-checks trust.
std::optional<UpdateEntry> CheckUpdate(const std::string& baseUrl, const CheckRequest& req);
} // namespace pawlet::hwupdate
+105
View File
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "detect.h"
#include <cstdio>
#include <filesystem>
#include <map>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/strings.h>
namespace pawlet::hwupdate {
namespace {
namespace fs = std::filesystem;
struct UsbDev {
std::string vid; // lowercase hex, e.g. "1209"
std::string pid; // lowercase hex
std::string node; // /dev/bus/usb/BBB/DDD
};
std::string ReadTrimmed(const std::string& path) {
std::string s;
if (!android::base::ReadFileToString(path, &s)) return {};
return android::base::Trim(s);
}
// Enumerate connected USB devices from sysfs.
std::vector<UsbDev> EnumUsb() {
std::vector<UsbDev> out;
const std::string base = "/sys/bus/usb/devices";
std::error_code ec;
if (!fs::exists(base, ec)) return out;
for (const auto& entry : fs::directory_iterator(base, ec)) {
const std::string dir = entry.path().string();
std::string vid = ReadTrimmed(dir + "/idVendor");
std::string pid = ReadTrimmed(dir + "/idProduct");
if (vid.empty() || pid.empty()) continue; // hubs/interfaces, skip
UsbDev d;
d.vid = android::base::Trim(vid);
d.pid = android::base::Trim(pid);
std::string bus = ReadTrimmed(dir + "/busnum");
std::string dev = ReadTrimmed(dir + "/devnum");
if (!bus.empty() && !dev.empty()) {
char node[64];
snprintf(node, sizeof(node), "/dev/bus/usb/%03d/%03d", atoi(bus.c_str()),
atoi(dev.c_str()));
d.node = node;
}
out.push_back(std::move(d));
}
return out;
}
bool UsbMatch(const std::vector<UsbDev>& devs, const std::string& vid, const std::string& pid,
std::string* node) {
for (const auto& d : devs) {
if (android::base::EqualsIgnoreCase(d.vid, vid) &&
android::base::EqualsIgnoreCase(d.pid, pid)) {
if (node) *node = d.node;
return true;
}
}
return false;
}
} // namespace
std::vector<DetectedComponent> Detect(const Manifest& manifest) {
std::vector<UsbDev> usb = EnumUsb();
std::vector<DetectedComponent> out;
for (const auto& prod : manifest.products) {
for (const auto& comp : prod.components) {
DetectedComponent dc;
dc.product = &prod;
dc.comp = &comp;
if (!comp.usbVendorId.empty() && !comp.usbProductId.empty()) {
std::string node;
if (UsbMatch(usb, comp.usbVendorId, comp.usbProductId, &node)) {
dc.present = true;
dc.transport = "usb";
dc.usbVendorId = comp.usbVendorId;
dc.usbProductId = comp.usbProductId;
dc.devPath = node;
}
}
// TODO: device-tree / i2c / spi detectors go here.
if (dc.present) {
LOG(INFO) << "hwupdate: detected " << comp.id << " on " << dc.transport;
}
out.push_back(std::move(dc));
}
}
return out;
}
} // namespace pawlet::hwupdate
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <vector>
#include "types.h"
namespace pawlet::hwupdate {
// Detect which manifest components are physically present. For USB components
// this enumerates /sys/bus/usb/devices and matches idVendor/idProduct; other
// transports report not-present until their detector is added. Pointers in the
// returned entries reference into the passed-in manifest, which must outlive
// the result.
std::vector<DetectedComponent> Detect(const Manifest& manifest);
} // namespace pawlet::hwupdate
+218
View File
@@ -0,0 +1,218 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "engine.h"
#include <algorithm>
#include <filesystem>
#include <android-base/logging.h>
#include "catalog.h"
#include "detect.h"
#include "http.h"
#include "manifest.h"
#include "pawlet/hwh_abi.h"
#include "util.h"
namespace pawlet::hwupdate {
namespace {
namespace fs = std::filesystem;
bool Contains(const std::vector<std::string>& v, const std::string& s) {
return std::find(v.begin(), v.end(), s) != v.end();
}
void ProgressCb(int percent, void* user) {
const char* id = static_cast<const char*>(user);
LOG(INFO) << "hwupdate: flashing " << (id ? id : "?") << " " << percent << "%";
}
} // namespace
Engine::Engine(EngineConfig cfg) : cfg_(std::move(cfg)), state_(cfg_.stateDir + "/state.json") {}
Engine::~Engine() { Stop(); }
void Engine::Start() {
std::error_code ec;
fs::create_directories(cfg_.stateDir, ec);
fs::create_directories(cfg_.stateDir + "/downloads", ec);
worker_ = std::thread([this] { WorkerLoop(); });
CheckNow(); // initial cycle at boot
}
void Engine::Stop() {
{
std::lock_guard<std::mutex> lk(reqMu_);
if (stop_) return;
stop_ = true;
reqCv_.notify_all();
}
if (worker_.joinable()) worker_.join();
}
void Engine::CheckNow() {
std::lock_guard<std::mutex> lk(reqMu_);
requested_ = true;
reqCv_.notify_all();
}
void Engine::WorkerLoop() {
for (;;) {
{
std::unique_lock<std::mutex> lk(reqMu_);
reqCv_.wait(lk, [this] { return requested_ || stop_; });
if (stop_) return;
requested_ = false;
}
busy_.store(true);
int rc = RunCycle();
lastResult_.store(rc);
busy_.store(false);
}
}
int Engine::RunCycle() {
auto manifest = LoadManifest(cfg_.manifestPath);
if (!manifest) return -1;
state_.Load();
std::vector<DetectedComponent> detected = Detect(*manifest);
std::vector<ComponentStatus> comps;
std::vector<PendingUpdate> pend;
for (auto& dc : detected) {
const ManifestProduct* prod = dc.product;
const ManifestComponent* comp = dc.comp;
ComponentStatus cs;
cs.componentId = comp->id;
cs.productId = prod->id;
cs.componentClass = comp->cls;
cs.role = comp->role;
cs.transport = dc.transport;
cs.present = dc.present;
cs.hardwareRevision = dc.hardwareRev;
cs.support = comp->support;
if (!dc.present) {
comps.push_back(std::move(cs));
continue;
}
// Resolve a usable handler + read the installed version.
const pawlet_hwh_api* handler = nullptr;
for (const auto& hid : comp->installerHandlers) {
handler = handlers_.Get(hid);
if (handler) break;
}
pawlet_hw_ctx ctx{};
ctx.component_id = comp->id.c_str();
ctx.transport = dc.transport.c_str();
ctx.usb_vendor_id = dc.usbVendorId.c_str();
ctx.usb_product_id = dc.usbProductId.c_str();
ctx.dev_path = dc.devPath.c_str();
ctx.hardware_rev = dc.hardwareRev.c_str();
std::string installed = state_.InstalledVersion(comp->id);
if (handler) {
char ver[128] = {0};
if (handler->get_version(&ctx, ver, sizeof(ver)) == PAWLET_HWH_OK && ver[0]) {
installed = ver;
}
}
cs.installedVersion = installed;
// Ask the server for the newest compatible update.
CheckRequest req;
req.device = manifest->device;
req.pawletVersion = manifest->buildVersion;
req.hardwareId = prod->id;
req.componentId = comp->id;
req.hardwareRevision = dc.hardwareRev;
req.currentVersion = installed;
auto update = CheckUpdate(cfg_.serverBaseUrl, req);
PendingUpdate pu;
pu.componentId = comp->id;
if (update && update->version != installed) {
pu.available = true;
pu.version = update->version;
pu.sizeBytes = update->sizeBytes;
pu.changelogUrl = update->changelogUrl;
// Trust boundary: the server-named handler MUST be allowlisted for
// this component in the immutable manifest.
if (!Contains(comp->installerHandlers, update->installerHandler)) {
LOG(ERROR) << "hwupdate: refusing non-allowlisted handler '"
<< update->installerHandler << "' for " << comp->id;
} else if (cfg_.autoApply) {
if (state_.RetryCount(comp->id) >= cfg_.maxRetries) {
LOG(WARNING) << "hwupdate: " << comp->id << " exceeded retries; skipping";
} else {
const pawlet_hwh_api* flashHandler = handlers_.Get(update->installerHandler);
std::string dl = cfg_.stateDir + "/downloads/" + update->id + "-" +
update->filename;
bool ok = flashHandler && http::Download(update->url, dl);
if (ok) {
std::string sum = util::Sha256File(dl);
if (!util::IEquals(sum, update->checksumSha256)) {
LOG(ERROR) << "hwupdate: checksum mismatch for " << comp->id;
ok = false;
}
}
if (ok) {
int r = flashHandler->flash(&ctx, dl.c_str(), ProgressCb,
const_cast<char*>(comp->id.c_str()));
if (r == PAWLET_HWH_OK) {
LOG(INFO) << "hwupdate: flashed " << comp->id << " -> "
<< update->version;
state_.SetInstalledVersion(comp->id, update->version);
state_.SetRetryCount(comp->id, 0);
cs.installedVersion = update->version;
pu.available = false; // applied
} else {
LOG(ERROR) << "hwupdate: flash failed rc=" << r << " for " << comp->id;
state_.SetRetryCount(comp->id, state_.RetryCount(comp->id) + 1);
ok = false;
}
} else {
state_.SetRetryCount(comp->id, state_.RetryCount(comp->id) + 1);
}
std::error_code ec;
fs::remove(dl, ec);
}
}
}
if (pu.available) pend.push_back(std::move(pu));
comps.push_back(std::move(cs));
}
state_.Save();
{
std::lock_guard<std::mutex> lk(snapMu_);
components_ = std::move(comps);
pending_ = std::move(pend);
}
return 0;
}
std::vector<ComponentStatus> Engine::Components() const {
std::lock_guard<std::mutex> lk(snapMu_);
return components_;
}
std::vector<PendingUpdate> Engine::Pending() const {
std::lock_guard<std::mutex> lk(snapMu_);
return pending_;
}
} // namespace pawlet::hwupdate
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "handlers.h"
#include "state.h"
namespace pawlet::hwupdate {
// Thread-safe snapshots exposed to the AIDL layer.
struct ComponentStatus {
std::string componentId;
std::string productId;
std::string componentClass;
std::string role;
std::string transport;
bool present = false;
std::string installedVersion;
std::string hardwareRevision;
std::string support;
};
struct PendingUpdate {
std::string componentId;
bool available = false;
std::string version;
int64_t sizeBytes = 0;
std::string changelogUrl;
};
struct EngineConfig {
std::string manifestPath;
std::string serverBaseUrl; // ".../ota.php?mode=hardware"
std::string stateDir; // e.g. /data/pawlet/hw
int maxRetries = 3;
bool autoApply = true;
};
class Engine {
public:
explicit Engine(EngineConfig cfg);
~Engine();
void Start(); // spawns worker, runs an initial cycle
void Stop();
// Request an asynchronous detect+check(+apply) cycle.
void CheckNow();
// Snapshots (safe to call from binder threads).
std::vector<ComponentStatus> Components() const;
std::vector<PendingUpdate> Pending() const;
bool IsBusy() const { return busy_.load(); }
int LastResult() const { return lastResult_.load(); }
private:
void WorkerLoop();
int RunCycle(); // returns 0 on success
EngineConfig cfg_;
HandlerRegistry handlers_;
State state_;
mutable std::mutex snapMu_;
std::vector<ComponentStatus> components_;
std::vector<PendingUpdate> pending_;
std::thread worker_;
std::mutex reqMu_;
std::condition_variable reqCv_;
bool requested_ = false;
bool stop_ = false;
std::atomic<bool> busy_{false};
std::atomic<int> lastResult_{0};
};
} // namespace pawlet::hwupdate
+69
View File
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "handlers.h"
#include <dlfcn.h>
#include <android-base/logging.h>
#include "util.h"
namespace pawlet::hwupdate {
namespace {
constexpr char kHandlerDir[] = "/system_ext/lib64/pawlet/hwh/";
// Handler ids come from the local allowlist; still, only accept a conservative
// character set so the id can never escape kHandlerDir.
bool ValidId(const std::string& id) {
if (id.empty() || id.size() > 64) return false;
for (char c : id) {
bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-';
if (!ok) return false;
}
return true;
}
} // namespace
const pawlet_hwh_api* HandlerRegistry::Get(const std::string& id) {
if (auto it = cache_.find(id); it != cache_.end()) return it->second;
if (!ValidId(id)) {
LOG(ERROR) << "hwupdate: rejecting invalid handler id '" << id << "'";
return nullptr;
}
std::string path = std::string(kHandlerDir) + "libpawlethwh_" + id + ".so";
void* dl = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!dl) {
LOG(ERROR) << "hwupdate: dlopen(" << path << ") failed: " << dlerror();
return nullptr;
}
auto entry = reinterpret_cast<const pawlet_hwh_api* (*)()>(dlsym(dl, "pawlet_hwh_entry"));
if (!entry) {
LOG(ERROR) << "hwupdate: " << path << " missing pawlet_hwh_entry";
dlclose(dl);
return nullptr;
}
const pawlet_hwh_api* api = entry();
if (!api || api->abi_version != PAWLET_HWH_ABI || !api->handler_id ||
!util::IEquals(api->handler_id, id) || !api->detect || !api->get_version ||
!api->flash) {
LOG(ERROR) << "hwupdate: " << path << " failed ABI/id validation";
dlclose(dl);
return nullptr;
}
LOG(INFO) << "hwupdate: loaded handler '" << id << "' (abi " << api->abi_version << ")";
// Intentionally keep dl open for the daemon's lifetime.
cache_[id] = api;
return api;
}
} // namespace pawlet::hwupdate
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <map>
#include <string>
#include "pawlet/hwh_abi.h"
namespace pawlet::hwupdate {
// Loads handler plugins by id from the fixed, dm-verity-protected directory
// /system_ext/lib64/pawlet/hwh/libpawlethwh_<id>.so. The id is validated by the
// caller against the component's installer_handlers allowlist BEFORE calling
// Get(); this class additionally verifies the ABI version and that the plugin's
// self-reported handler_id matches the requested id. It never derives a path
// from untrusted input other than the (allowlisted) id.
class HandlerRegistry {
public:
// Returns the handler vtable for id, or nullptr if it cannot be loaded or
// fails validation. Loaded handlers are cached for the daemon's lifetime.
const pawlet_hwh_api* Get(const std::string& id);
private:
std::map<std::string, const pawlet_hwh_api*> cache_;
};
} // namespace pawlet::hwupdate
+96
View File
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "http.h"
#include <curl/curl.h>
#include <cstdio>
#include <android-base/logging.h>
namespace pawlet::hwupdate::http {
namespace {
constexpr long kConnectTimeoutSec = 15;
constexpr long kTransferTimeoutSec = 300;
constexpr char kUserAgent[] = "pawlethwd/1.0";
size_t WriteToString(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* out = static_cast<std::string*>(userdata);
out->append(ptr, size * nmemb);
return size * nmemb;
}
size_t WriteToFile(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* f = static_cast<FILE*>(userdata);
return fwrite(ptr, size, nmemb, f) * size;
}
void ApplyCommon(CURL* c, const std::string& url) {
curl_easy_setopt(c, CURLOPT_URL, url.c_str());
curl_easy_setopt(c, CURLOPT_USERAGENT, kUserAgent);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_MAXREDIRS, 5L);
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, kConnectTimeoutSec);
curl_easy_setopt(c, CURLOPT_TIMEOUT, kTransferTimeoutSec);
curl_easy_setopt(c, CURLOPT_FAILONERROR, 0L);
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
// HTTPS only, verified against the system CA store.
curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(c, CURLOPT_CAPATH, "/system/etc/security/cacerts");
}
} // namespace
bool Get(const std::string& url, std::string* body, long* httpCode) {
CURL* c = curl_easy_init();
if (!c) return false;
body->clear();
ApplyCommon(c, url);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, WriteToString);
curl_easy_setopt(c, CURLOPT_WRITEDATA, body);
CURLcode rc = curl_easy_perform(c);
if (rc == CURLE_OK && httpCode) {
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, httpCode);
}
curl_easy_cleanup(c);
if (rc != CURLE_OK) {
LOG(WARNING) << "hwupdate: GET failed: " << curl_easy_strerror(rc);
return false;
}
return true;
}
bool Download(const std::string& url, const std::string& destPath) {
FILE* f = fopen(destPath.c_str(), "wb");
if (!f) {
LOG(ERROR) << "hwupdate: cannot open " << destPath << " for write";
return false;
}
CURL* c = curl_easy_init();
if (!c) {
fclose(f);
return false;
}
ApplyCommon(c, url);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, WriteToFile);
curl_easy_setopt(c, CURLOPT_WRITEDATA, f);
CURLcode rc = curl_easy_perform(c);
long code = 0;
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code);
curl_easy_cleanup(c);
fclose(f);
if (rc != CURLE_OK || code != 200) {
LOG(WARNING) << "hwupdate: download failed rc=" << curl_easy_strerror(rc)
<< " http=" << code;
remove(destPath.c_str());
return false;
}
return true;
}
} // namespace pawlet::hwupdate::http
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <string>
namespace pawlet::hwupdate::http {
// GET url. On success returns true and sets body + httpCode. Network/transport
// failures return false.
bool Get(const std::string& url, std::string* body, long* httpCode);
// Download url to destPath. Returns true only on HTTP 200 with the body fully
// written to destPath.
bool Download(const std::string& url, const std::string& destPath);
} // namespace pawlet::hwupdate::http
+14
View File
@@ -0,0 +1,14 @@
# Copyright (C) 2026 oxmc / PawletOS
# SPDX-License-Identifier: Apache-2.0
on post-fs-data
mkdir /data/pawlet 0771 root system
mkdir /data/pawlet/hw 0770 root system
service pawlethwd /system_ext/bin/pawlethwd
class late_start
user root
group root system inet
# Long-running binder service; init restarts it if it exits.
# Detects peripherals, checks the hardware update catalog, and flashes
# firmware via authorized handler plugins. Starts after /data is mounted.
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include <curl/curl.h>
#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <binder/ProcessState.h>
#include <utils/String16.h>
#include <android-base/logging.h>
#include "engine.h"
#include "service.h"
#include "util.h"
using namespace pawlet::hwupdate;
namespace {
constexpr char kServiceName[] = "pawlet_hwupdate";
constexpr char kDefaultManifest[] = "/system_ext/etc/pawlet/supported_hardware.json";
constexpr char kDefaultServer[] = "https://oxmc.me/apis/aosp/ota.php?mode=hardware";
constexpr char kStateDir[] = "/data/pawlet/hw";
} // namespace
int main(int, char**) {
android::base::InitLogging(nullptr, android::base::LogdLogger());
LOG(INFO) << "pawlethwd starting";
if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) {
LOG(ERROR) << "pawlethwd: curl_global_init failed";
return 1;
}
EngineConfig cfg;
cfg.manifestPath = util::Prop("ro.pawlet.hardware.manifest", kDefaultManifest);
cfg.serverBaseUrl = util::Prop("ro.pawlet.hwupdate.uri", kDefaultServer);
cfg.stateDir = kStateDir;
cfg.autoApply = util::Prop("persist.pawlet.hwupdate.auto_apply", "true") != "false";
Engine engine(cfg);
engine.Start();
android::sp<HwUpdateService> service = android::sp<HwUpdateService>::make(&engine);
android::status_t st = android::defaultServiceManager()->addService(
android::String16(kServiceName), service);
if (st != android::OK) {
LOG(ERROR) << "pawlethwd: failed to register service: " << st;
return 1;
}
LOG(INFO) << "pawlethwd: service registered as " << kServiceName;
android::ProcessState::self()->startThreadPool();
android::IPCThreadState::self()->joinThreadPool();
engine.Stop();
curl_global_cleanup();
return 0;
}
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "manifest.h"
#include <json/json.h>
#include <fstream>
#include <android-base/logging.h>
namespace pawlet::hwupdate {
namespace {
std::vector<std::string> StrArray(const Json::Value& v) {
std::vector<std::string> out;
if (v.isArray()) {
for (const auto& e : v) {
if (e.isString()) out.push_back(e.asString());
}
}
return out;
}
void ReadIdentifiers(const Json::Value& ids, std::string& usbVid, std::string& usbPid,
std::vector<std::string>& dtCompat) {
if (!ids.isObject()) return;
usbVid = ids.get("usb_vendor_id", "").asString();
usbPid = ids.get("usb_product_id", "").asString();
dtCompat = StrArray(ids["device_tree_compatible"]);
}
} // namespace
std::optional<Manifest> LoadManifest(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
LOG(ERROR) << "hwupdate: cannot open manifest " << path;
return std::nullopt;
}
Json::Value root;
Json::CharReaderBuilder b;
std::string errs;
if (!Json::parseFromStream(b, in, &root, &errs)) {
LOG(ERROR) << "hwupdate: manifest parse error: " << errs;
return std::nullopt;
}
Manifest m;
m.schemaVersion = root.get("schema_version", 0).asInt();
if (m.schemaVersion != 1) {
LOG(ERROR) << "hwupdate: unsupported manifest schema_version " << m.schemaVersion;
return std::nullopt;
}
m.device = root.get("device", "").asString();
m.buildVersion = root.get("build_version", "").asString();
for (const auto& p : root["supported_hardware"]) {
ManifestProduct prod;
prod.id = p.get("id", "").asString();
prod.manufacturer = p.get("manufacturer", "").asString();
prod.cls = p.get("class", "").asString();
prod.support = p.get("support", "full").asString();
prod.supportedRevisions = StrArray(p["supported_revisions"]);
ReadIdentifiers(p["identifiers"], prod.usbVendorId, prod.usbProductId, prod.dtCompatible);
for (const auto& c : p["components"]) {
ManifestComponent comp;
comp.id = c.get("id", "").asString();
comp.cls = c.get("class", "").asString();
comp.role = c.get("role", "").asString();
comp.support = c.get("support", "full").asString();
comp.supportedRevisions = StrArray(c["supported_revisions"]);
comp.installerHandlers = StrArray(c["installer_handlers"]);
ReadIdentifiers(c["identifiers"], comp.usbVendorId, comp.usbProductId, comp.dtCompatible);
// Fall back to the product's identifiers if the component omits them.
if (comp.usbVendorId.empty()) comp.usbVendorId = prod.usbVendorId;
if (comp.usbProductId.empty()) comp.usbProductId = prod.usbProductId;
if (comp.dtCompatible.empty()) comp.dtCompatible = prod.dtCompatible;
prod.components.push_back(std::move(comp));
}
m.products.push_back(std::move(prod));
}
LOG(INFO) << "hwupdate: loaded manifest device=" << m.device
<< " products=" << m.products.size();
return m;
}
} // namespace pawlet::hwupdate
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <optional>
#include <string>
#include "types.h"
namespace pawlet::hwupdate {
// Parse the image-local supported_hardware.json at path. Returns nullopt on
// read/parse error or unsupported schema version.
std::optional<Manifest> LoadManifest(const std::string& path);
} // namespace pawlet::hwupdate
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "service.h"
namespace pawlet::hwupdate {
namespace aidl = ::me::pawlet::hardware::update;
using ::android::binder::Status;
Status HwUpdateService::getComponents(::std::vector<aidl::HwComponent>* out) {
out->clear();
for (const auto& c : engine_->Components()) {
aidl::HwComponent hc;
hc.componentId = c.componentId;
hc.productId = c.productId;
hc.componentClass = c.componentClass;
hc.role = c.role;
hc.transport = c.transport;
hc.present = c.present;
hc.installedVersion = c.installedVersion;
hc.hardwareRevision = c.hardwareRevision;
hc.support = c.support;
out->push_back(std::move(hc));
}
return Status::ok();
}
Status HwUpdateService::getPendingUpdates(::std::vector<aidl::HwUpdateInfo>* out) {
out->clear();
for (const auto& p : engine_->Pending()) {
aidl::HwUpdateInfo hu;
hu.componentId = p.componentId;
hu.available = p.available;
hu.version = p.version;
hu.sizeBytes = p.sizeBytes;
hu.changelogUrl = p.changelogUrl;
out->push_back(std::move(hu));
}
return Status::ok();
}
Status HwUpdateService::checkNow() {
engine_->CheckNow();
return Status::ok();
}
Status HwUpdateService::isBusy(bool* out) {
*out = engine_->IsBusy();
return Status::ok();
}
Status HwUpdateService::getLastResult(int32_t* out) {
*out = engine_->LastResult();
return Status::ok();
}
} // namespace pawlet::hwupdate
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <me/pawlet/hardware/update/BnPawletHardwareUpdate.h>
#include "engine.h"
namespace pawlet::hwupdate {
// Binder front-end for the daemon; delegates to Engine. Registered as
// "pawlet_hwupdate".
class HwUpdateService : public me::pawlet::hardware::update::BnPawletHardwareUpdate {
public:
explicit HwUpdateService(Engine* engine) : engine_(engine) {}
::android::binder::Status getComponents(
::std::vector<::me::pawlet::hardware::update::HwComponent>* out) override;
::android::binder::Status getPendingUpdates(
::std::vector<::me::pawlet::hardware::update::HwUpdateInfo>* out) override;
::android::binder::Status checkNow() override;
::android::binder::Status isBusy(bool* out) override;
::android::binder::Status getLastResult(int32_t* out) override;
private:
Engine* engine_;
};
} // namespace pawlet::hwupdate
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "state.h"
#include <json/json.h>
#include <fstream>
#include <android-base/logging.h>
namespace pawlet::hwupdate {
State::State(std::string path) : path_(std::move(path)) {}
void State::Load() {
entries_.clear();
std::ifstream in(path_, std::ios::binary);
if (!in) return; // first run
Json::Value root;
Json::CharReaderBuilder b;
std::string errs;
if (!Json::parseFromStream(b, in, &root, &errs)) {
LOG(WARNING) << "hwupdate: state parse error: " << errs;
return;
}
const Json::Value& comps = root["components"];
for (const auto& id : comps.getMemberNames()) {
Entry e;
e.installedVersion = comps[id].get("installed_version", "").asString();
e.retryCount = comps[id].get("retry_count", 0).asInt();
entries_[id] = e;
}
}
bool State::Save() const {
Json::Value root;
root["schema_version"] = 1;
Json::Value comps(Json::objectValue);
for (const auto& [id, e] : entries_) {
Json::Value c(Json::objectValue);
c["installed_version"] = e.installedVersion;
c["retry_count"] = e.retryCount;
comps[id] = c;
}
root["components"] = comps;
// Write atomically: temp file + rename.
std::string tmp = path_ + ".tmp";
{
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
if (!out) {
LOG(ERROR) << "hwupdate: cannot write state " << tmp;
return false;
}
Json::StreamWriterBuilder w;
out << Json::writeString(w, root);
}
if (rename(tmp.c_str(), path_.c_str()) != 0) {
LOG(ERROR) << "hwupdate: cannot rename state into place";
return false;
}
return true;
}
std::string State::InstalledVersion(const std::string& componentId) const {
auto it = entries_.find(componentId);
return it == entries_.end() ? std::string() : it->second.installedVersion;
}
void State::SetInstalledVersion(const std::string& componentId, const std::string& version) {
entries_[componentId].installedVersion = version;
}
int State::RetryCount(const std::string& componentId) const {
auto it = entries_.find(componentId);
return it == entries_.end() ? 0 : it->second.retryCount;
}
void State::SetRetryCount(const std::string& componentId, int count) {
entries_[componentId].retryCount = count;
}
} // namespace pawlet::hwupdate
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <map>
#include <string>
namespace pawlet::hwupdate {
// Persistent per-component state: last successfully installed firmware version
// and a retry counter. Backed by a JSON file under /data/pawlet/hw.
class State {
public:
explicit State(std::string path);
void Load();
bool Save() const;
std::string InstalledVersion(const std::string& componentId) const;
void SetInstalledVersion(const std::string& componentId, const std::string& version);
int RetryCount(const std::string& componentId) const;
void SetRetryCount(const std::string& componentId, int count);
private:
struct Entry {
std::string installedVersion;
int retryCount = 0;
};
std::string path_;
std::map<std::string, Entry> entries_;
};
} // namespace pawlet::hwupdate
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace pawlet::hwupdate {
// ---- image-local manifest (supported_hardware.json) ----
struct ManifestComponent {
std::string id;
std::string cls;
std::string role;
std::string support;
std::vector<std::string> supportedRevisions;
std::vector<std::string> installerHandlers;
// detection identifiers (optional; may also come from the catalog)
std::string usbVendorId;
std::string usbProductId;
std::vector<std::string> dtCompatible;
};
struct ManifestProduct {
std::string id;
std::string manufacturer;
std::string cls;
std::string support;
std::vector<std::string> supportedRevisions;
std::string usbVendorId;
std::string usbProductId;
std::vector<std::string> dtCompatible;
std::vector<ManifestComponent> components;
};
struct Manifest {
int schemaVersion = 0;
std::string device;
std::string buildVersion;
std::vector<ManifestProduct> products;
};
// ---- server update entry (ota.php?mode=hardware&target=update) ----
struct UpdateEntry {
std::string version;
int64_t datetime = 0;
std::string filename;
std::string id;
int64_t sizeBytes = 0;
std::string url;
std::string checksumSha256;
std::string changelogUrl;
std::string installerType; // usb/i2c/...
std::string installerHandler; // handler id named by the server
};
// ---- runtime detection result ----
struct DetectedComponent {
const ManifestProduct* product = nullptr;
const ManifestComponent* comp = nullptr;
bool present = false;
std::string installedVersion;
std::string hardwareRev;
std::string transport; // resolved transport for this component
std::string usbVendorId;
std::string usbProductId;
std::string devPath;
};
} // namespace pawlet::hwupdate
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#include "util.h"
#include <openssl/sha.h>
#include <array>
#include <cctype>
#include <cstdio>
#include <android-base/properties.h>
namespace pawlet::hwupdate::util {
std::string Sha256File(const std::string& path) {
FILE* f = fopen(path.c_str(), "rb");
if (!f) return {};
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::array<unsigned char, 65536> buf;
size_t n;
while ((n = fread(buf.data(), 1, buf.size(), f)) > 0) {
SHA256_Update(&ctx, buf.data(), n);
}
bool ok = feof(f) != 0;
fclose(f);
if (!ok) return {};
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256_Final(digest, &ctx);
static const char* hex = "0123456789abcdef";
std::string out;
out.reserve(SHA256_DIGEST_LENGTH * 2);
for (unsigned char c : digest) {
out.push_back(hex[c >> 4]);
out.push_back(hex[c & 0xf]);
}
return out;
}
bool IEquals(const std::string& a, const std::string& b) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(a[i])) !=
std::tolower(static_cast<unsigned char>(b[i]))) {
return false;
}
}
return true;
}
std::string Prop(const std::string& key, const std::string& def) {
return android::base::GetProperty(key, def);
}
} // namespace pawlet::hwupdate::util
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <string>
namespace pawlet::hwupdate::util {
// Lowercase hex SHA-256 of a file's contents, or empty on error.
std::string Sha256File(const std::string& path);
// Case-insensitive equality (for hex checksums / handler ids).
bool IEquals(const std::string& a, const std::string& b);
// Read an Android system property with a default.
std::string Prop(const std::string& key, const std::string& def = "");
} // namespace pawlet::hwupdate::util
@@ -0,0 +1,32 @@
{
"schema_version": 1,
"device": "rpi4",
"build_version": "1.0",
"supported_hardware": [
{
"id": "pawlet-usb-dfu-reference",
"manufacturer": "pawlet",
"class": "peripheral",
"support": "experimental",
"supported_revisions": ["A", "B"],
"identifiers": {
"usb_vendor_id": "1209",
"usb_product_id": "0001"
},
"components": [
{
"id": "pawlet-usb-dfu-reference-mcu",
"class": "microcontroller",
"role": "peripheral-mcu",
"support": "experimental",
"supported_revisions": ["A", "B"],
"installer_handlers": ["usbdfu"],
"identifiers": {
"usb_vendor_id": "1209",
"usb_product_id": "0001"
}
}
]
}
]
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2026 oxmc / PawletOS
// SPDX-License-Identifier: Apache-2.0
cc_library_shared {
name: "libpawlethwh_usbdfu",
system_ext_specific: true,
compile_multilib: "64",
relative_install_path: "pawlet/hwh",
srcs: ["usbdfu.cpp"],
header_libs: ["pawlet_hwh_headers"],
shared_libs: [
"libbase",
"liblog",
"libusb",
],
cflags: [
"-Wall",
"-Werror",
"-Wextra",
],
}
+259
View File
@@ -0,0 +1,259 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*
* USB DFU (Device Firmware Upgrade, class 0xFE / subclass 0x01) handler plugin.
* Implements the plain DFU 1.1 download sequence over libusb. DfuSe/ST vendor
* extensions are out of scope for this handler.
*/
#include <libusb/libusb.h>
#include <unistd.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <android-base/logging.h>
#include "pawlet/hwh_abi.h"
namespace {
// DFU class requests.
constexpr uint8_t DFU_DNLOAD = 1;
constexpr uint8_t DFU_GETSTATUS = 3;
constexpr uint8_t DFU_CLRSTATUS = 4;
constexpr uint8_t DFU_ABORT = 6;
// DFU states.
constexpr uint8_t dfuIDLE = 2;
constexpr uint8_t dfuDNLOAD_IDLE = 5;
constexpr uint8_t dfuERROR = 10;
constexpr uint8_t OUT_IFACE = 0x21; // host->device | class | interface
constexpr uint8_t IN_IFACE = 0xA1; // device->host | class | interface
constexpr uint16_t DEFAULT_TRANSFER_SIZE = 1024;
constexpr unsigned CTRL_TIMEOUT_MS = 5000;
struct DfuStatus {
uint8_t bStatus;
uint32_t bwPollTimeout;
uint8_t bState;
uint8_t iString;
};
uint16_t ParseHex16(const char* s) {
if (!s || !*s) return 0;
return static_cast<uint16_t>(strtoul(s, nullptr, 16));
}
bool ReadFile(const char* path, std::vector<uint8_t>* out) {
FILE* f = fopen(path, "rb");
if (!f) return false;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
if (len < 0) {
fclose(f);
return false;
}
out->resize(static_cast<size_t>(len));
size_t n = fread(out->data(), 1, out->size(), f);
fclose(f);
return n == out->size();
}
libusb_device_handle* OpenByCtx(libusb_context* ctx, const pawlet_hw_ctx* c) {
uint16_t vid = ParseHex16(c->usb_vendor_id);
uint16_t pid = ParseHex16(c->usb_product_id);
if (!vid || !pid) return nullptr;
return libusb_open_device_with_vid_pid(ctx, vid, pid);
}
int GetStatus(libusb_device_handle* h, int iface, DfuStatus* st) {
uint8_t buf[6] = {0};
int r = libusb_control_transfer(h, IN_IFACE, DFU_GETSTATUS, 0, iface, buf, sizeof(buf),
CTRL_TIMEOUT_MS);
if (r != 6) return PAWLET_HWH_IO;
st->bStatus = buf[0];
st->bwPollTimeout = buf[1] | (buf[2] << 8) | (buf[3] << 16);
st->bState = buf[4];
st->iString = buf[5];
return PAWLET_HWH_OK;
}
// Locate the DFU interface number and its wTransferSize from the config.
bool FindDfuInterface(libusb_device* dev, int* ifaceOut, uint16_t* transferSize) {
libusb_config_descriptor* cfg = nullptr;
if (libusb_get_active_config_descriptor(dev, &cfg) != 0 || !cfg) return false;
bool found = false;
for (int i = 0; i < cfg->bNumInterfaces && !found; ++i) {
const libusb_interface& intf = cfg->interface[i];
for (int a = 0; a < intf.num_altsetting; ++a) {
const libusb_interface_descriptor& d = intf.altsetting[a];
if (d.bInterfaceClass == 0xFE && d.bInterfaceSubClass == 0x01) {
*ifaceOut = d.bInterfaceNumber;
*transferSize = DEFAULT_TRANSFER_SIZE;
// DFU functional descriptor (bDescriptorType 0x21) carries
// wTransferSize at offset 5..6.
const uint8_t* extra = d.extra;
int len = d.extra_length;
while (len >= 2) {
uint8_t blen = extra[0];
uint8_t btype = extra[1];
if (blen < 2 || blen > len) break;
if (btype == 0x21 && blen >= 7) {
uint16_t ts = extra[5] | (extra[6] << 8);
if (ts > 0) *transferSize = ts;
}
extra += blen;
len -= blen;
}
found = true;
break;
}
}
}
libusb_free_config_descriptor(cfg);
return found;
}
// ---- ABI entry points ----
int Detect(const pawlet_hw_ctx* ctx) {
libusb_context* c = nullptr;
if (libusb_init(&c) != 0) return PAWLET_HWH_ERR;
libusb_device_handle* h = OpenByCtx(c, ctx);
int rc = h ? PAWLET_HWH_OK : PAWLET_HWH_NOT_PRESENT;
if (h) libusb_close(h);
libusb_exit(c);
return rc;
}
int GetVersion(const pawlet_hw_ctx* ctx, char* out, size_t out_size) {
if (!out || out_size == 0) return PAWLET_HWH_ERR;
libusb_context* c = nullptr;
if (libusb_init(&c) != 0) return PAWLET_HWH_ERR;
libusb_device_handle* h = OpenByCtx(c, ctx);
if (!h) {
libusb_exit(c);
return PAWLET_HWH_NOT_PRESENT;
}
libusb_device_descriptor desc{};
int rc = PAWLET_HWH_UNSUPPORTED;
if (libusb_get_device_descriptor(libusb_get_device(h), &desc) == 0) {
// Report bcdDevice as the installed firmware version (common for DFU MCUs).
snprintf(out, out_size, "%u.%u", desc.bcdDevice >> 8, desc.bcdDevice & 0xff);
rc = PAWLET_HWH_OK;
}
libusb_close(h);
libusb_exit(c);
return rc;
}
int Flash(const pawlet_hw_ctx* ctx, const char* path, pawlet_progress_cb progress, void* user) {
std::vector<uint8_t> fw;
if (!ReadFile(path, &fw) || fw.empty()) return PAWLET_HWH_IO;
libusb_context* c = nullptr;
if (libusb_init(&c) != 0) return PAWLET_HWH_ERR;
libusb_device_handle* h = OpenByCtx(c, ctx);
if (!h) {
libusb_exit(c);
return PAWLET_HWH_NOT_PRESENT;
}
int iface = 0;
uint16_t transfer = DEFAULT_TRANSFER_SIZE;
if (!FindDfuInterface(libusb_get_device(h), &iface, &transfer)) {
LOG(ERROR) << "usbdfu: no DFU interface";
libusb_close(h);
libusb_exit(c);
return PAWLET_HWH_UNSUPPORTED;
}
if (libusb_kernel_driver_active(h, iface) == 1) libusb_detach_kernel_driver(h, iface);
if (libusb_claim_interface(h, iface) != 0) {
LOG(ERROR) << "usbdfu: claim_interface failed";
libusb_close(h);
libusb_exit(c);
return PAWLET_HWH_IO;
}
int result = PAWLET_HWH_OK;
DfuStatus st{};
// Ensure a clean idle state.
if (GetStatus(h, iface, &st) == PAWLET_HWH_OK && st.bState == dfuERROR) {
libusb_control_transfer(h, OUT_IFACE, DFU_CLRSTATUS, 0, iface, nullptr, 0, CTRL_TIMEOUT_MS);
}
size_t sent = 0;
uint16_t block = 0;
while (sent < fw.size()) {
uint16_t chunk = static_cast<uint16_t>(std::min<size_t>(transfer, fw.size() - sent));
int r = libusb_control_transfer(h, OUT_IFACE, DFU_DNLOAD, block, iface, fw.data() + sent,
chunk, CTRL_TIMEOUT_MS);
if (r < 0 || static_cast<uint16_t>(r) != chunk) {
LOG(ERROR) << "usbdfu: DNLOAD block " << block << " failed: " << r;
result = PAWLET_HWH_IO;
break;
}
// Wait for the device to program the block.
if (GetStatus(h, iface, &st) != PAWLET_HWH_OK) {
result = PAWLET_HWH_IO;
break;
}
if (st.bStatus != 0) {
LOG(ERROR) << "usbdfu: device error status=" << (int)st.bStatus;
result = PAWLET_HWH_IO;
break;
}
if (st.bwPollTimeout) usleep(st.bwPollTimeout * 1000);
sent += chunk;
++block;
if (progress) progress(static_cast<int>(sent * 100 / fw.size()), user);
}
if (result == PAWLET_HWH_OK) {
// Zero-length DNLOAD signals end-of-transfer; then manifest.
libusb_control_transfer(h, OUT_IFACE, DFU_DNLOAD, block, iface, nullptr, 0, CTRL_TIMEOUT_MS);
GetStatus(h, iface, &st);
if (st.bwPollTimeout) usleep(st.bwPollTimeout * 1000);
// Poll to idle / manifest complete.
for (int i = 0; i < 10; ++i) {
if (GetStatus(h, iface, &st) != PAWLET_HWH_OK) break;
if (st.bState == dfuIDLE || st.bState == dfuDNLOAD_IDLE) break;
if (st.bState == dfuERROR) {
result = PAWLET_HWH_VERIFY;
break;
}
usleep((st.bwPollTimeout ? st.bwPollTimeout : 100) * 1000);
}
} else {
libusb_control_transfer(h, OUT_IFACE, DFU_ABORT, 0, iface, nullptr, 0, CTRL_TIMEOUT_MS);
}
libusb_release_interface(h, iface);
libusb_close(h);
libusb_exit(c);
return result;
}
const pawlet_hwh_api kApi = {
/* abi_version */ PAWLET_HWH_ABI,
/* handler_id */ "usbdfu",
/* detect */ Detect,
/* get_version */ GetVersion,
/* flash */ Flash,
};
} // namespace
extern "C" const pawlet_hwh_api* pawlet_hwh_entry(void) { return &kApi; }
+20
View File
@@ -0,0 +1,20 @@
#
# Copyright (C) 2026 oxmc / PawletOS
# SPDX-License-Identifier: Apache-2.0
#
# PawletOS hardware update subsystem. Include from a product makefile:
# $(call inherit-product, vendor/pawlet/hwupdate/hwupdate.mk)
#
PRODUCT_PACKAGES += \
pawlethwd \
libpawlethwh_usbdfu \
pawlet_supported_hardware_default
# Path to the image-local supported-hardware manifest (daemon default matches
# this; set explicitly so it is discoverable/overridable).
PRODUCT_PRODUCT_PROPERTIES += \
ro.pawlet.hardware.manifest=/system_ext/etc/pawlet/supported_hardware.json
# Daemon SELinux policy (system_ext side).
SYSTEM_EXT_PRIVATE_SEPOLICY_DIRS += vendor/pawlet/hwupdate/sepolicy
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2026 oxmc / PawletOS
* SPDX-License-Identifier: Apache-2.0
*
* Stable C ABI for PawletOS hardware-update handler plugins.
*
* A handler is a shared library installed at
* /system_ext/lib{,64}/pawlet/hwh/libpawlethwh_<id>.so
* that exports exactly one symbol:
*
* const pawlet_hwh_api* pawlet_hwh_entry(void);
*
* The daemon dlopen()s the library only when <id> appears in the component's
* installer_handlers allowlist in the immutable supported_hardware.json, and
* refuses a vtable whose abi_version or handler_id do not match. Keep this ABI
* backwards compatible; bump PAWLET_HWH_ABI only on a breaking change.
*/
#ifndef PAWLET_HWH_ABI_H
#define PAWLET_HWH_ABI_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PAWLET_HWH_ABI 1u
/* Return codes for handler entry points. */
enum {
PAWLET_HWH_OK = 0,
PAWLET_HWH_ERR = -1, /* generic failure */
PAWLET_HWH_NOT_PRESENT = -2, /* detect(): device not found */
PAWLET_HWH_UNSUPPORTED = -3, /* operation not supported for this device */
PAWLET_HWH_IO = -4, /* transport I/O error */
PAWLET_HWH_VERIFY = -5 /* post-flash verification failed */
};
/*
* Context describing the target component for a handler call. Fields not
* relevant to a given transport may be empty. Owned by the daemon; valid only
* for the duration of the call.
*/
typedef struct {
const char* component_id; /* stable component id from the manifest */
const char* transport; /* "usb" | "i2c" | "spi" | "uart" | "gpio" | "can" */
const char* usb_vendor_id; /* hex string, e.g. "1209" (may be empty) */
const char* usb_product_id; /* hex string, e.g. "0001" (may be empty) */
const char* dev_path; /* transport device node, e.g. /dev/i2c-1 (may be empty) */
const char* hardware_rev; /* detected revision (may be empty) */
} pawlet_hw_ctx;
/* Progress callback: percent is 0..100. user is passed through from flash(). */
typedef void (*pawlet_progress_cb)(int percent, void* user);
typedef struct {
uint32_t abi_version; /* must equal PAWLET_HWH_ABI */
const char* handler_id; /* must match the <id> in the .so filename / allowlist */
/* Probe whether the target device is present/reachable on its transport.
* Returns PAWLET_HWH_OK if present, PAWLET_HWH_NOT_PRESENT otherwise. */
int (*detect)(const pawlet_hw_ctx* ctx);
/* Read the currently installed firmware version into out (NUL-terminated,
* up to out_size). Returns PAWLET_HWH_OK on success. May return
* PAWLET_HWH_UNSUPPORTED if the device cannot report its version. */
int (*get_version)(const pawlet_hw_ctx* ctx, char* out, size_t out_size);
/* Flash the verified firmware file at path to the device. progress may be
* NULL. Returns PAWLET_HWH_OK on success. */
int (*flash)(const pawlet_hw_ctx* ctx, const char* path,
pawlet_progress_cb progress, void* user);
} pawlet_hwh_api;
/* The single exported symbol every handler .so must provide. */
const pawlet_hwh_api* pawlet_hwh_entry(void);
#ifdef __cplusplus
}
#endif
#endif /* PAWLET_HWH_ABI_H */
@@ -0,0 +1,96 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://pawletos.oxmc.me/schemas/device-updates.schema.json",
"title": "PawletOS hardware update catalog",
"type": "object",
"required": ["schema_version", "manufacturers"],
"properties": {
"schema_version": { "type": "integer", "const": 1 },
"manufacturers": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/manufacturer" }
}
},
"definitions": {
"manufacturer": {
"type": "object",
"required": ["name", "classes"],
"properties": {
"name": { "type": "string" },
"classes": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["items"],
"properties": {
"items": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/product" }
}
}
}
}
}
},
"product": {
"type": "object",
"required": ["name", "class", "components"],
"properties": {
"name": { "type": "string" },
"model": { "type": "string" },
"class": { "type": "string" },
"connections": { "type": "object", "additionalProperties": { "type": "string" } },
"identifiers": { "type": "object" },
"components": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/component" }
}
}
},
"component": {
"type": "object",
"required": ["id", "class", "updates"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"class": { "type": "string" },
"role": { "type": "string" },
"identifiers": { "type": "object" },
"updates": { "type": "array", "items": { "$ref": "#/definitions/update" } }
}
},
"update": {
"type": "object",
"required": ["version", "datetime", "filename", "id", "size_bytes", "url", "checksum_sha256", "installer"],
"properties": {
"version": { "type": "string" },
"datetime": { "type": "integer" },
"filename": { "type": "string" },
"id": { "type": "string" },
"size_bytes": { "type": "integer", "minimum": 0 },
"url": { "type": "string", "format": "uri" },
"checksum_sha256": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" },
"changelog_url": { "type": "string" },
"compatibility": {
"type": "object",
"properties": {
"hardware_revisions": { "type": "array", "items": { "type": "string" } },
"pawlet_devices": { "type": "array", "items": { "type": "string" } },
"minimum_pawlet_version": { "type": "string" },
"maximum_pawlet_version": { "type": "string" },
"minimum_bootloader_version": { "type": "string" },
"maximum_bootloader_version": { "type": "string" }
}
},
"installer": {
"type": "object",
"required": ["type", "handler"],
"properties": {
"type": { "enum": ["usb", "i2c", "spi", "uart", "gpio", "can"] },
"handler": { "type": "string", "minLength": 1 }
}
}
}
}
}
}
@@ -0,0 +1,61 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://pawletos.oxmc.me/schemas/supported-hardware.schema.json",
"title": "PawletOS image-local supported hardware manifest",
"type": "object",
"required": ["schema_version", "device", "build_version", "supported_hardware"],
"properties": {
"schema_version": { "type": "integer", "const": 1 },
"device": { "type": "string", "minLength": 1 },
"build_version": { "type": "string", "minLength": 1 },
"supported_hardware": {
"type": "array",
"items": { "$ref": "#/definitions/product" }
}
},
"definitions": {
"support": { "enum": ["full", "experimental", "limited", "deprecated"] },
"product": {
"type": "object",
"required": ["id", "manufacturer", "class", "support", "components"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"manufacturer": { "type": "string", "minLength": 1 },
"class": { "type": "string", "minLength": 1 },
"support": { "$ref": "#/definitions/support" },
"supported_revisions": { "type": "array", "items": { "type": "string" } },
"identifiers": { "$ref": "#/definitions/identifiers" },
"components": {
"type": "array",
"items": { "$ref": "#/definitions/component" }
}
}
},
"component": {
"type": "object",
"required": ["id", "class", "support", "installer_handlers"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"class": { "type": "string", "minLength": 1 },
"role": { "type": "string" },
"support": { "$ref": "#/definitions/support" },
"supported_revisions": { "type": "array", "items": { "type": "string" } },
"installer_handlers": {
"type": "array",
"items": { "type": "string", "minLength": 1 }
},
"identifiers": { "$ref": "#/definitions/identifiers" }
}
},
"identifiers": {
"type": "object",
"properties": {
"device_tree_compatible": { "type": "array", "items": { "type": "string" } },
"usb_vendor_id": { "type": "string" },
"usb_product_id": { "type": "string" },
"hardware_revisions": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
}
}
}
+5
View File
@@ -0,0 +1,5 @@
# Copyright (C) 2026 oxmc / PawletOS
# SPDX-License-Identifier: Apache-2.0
/system_ext/bin/pawlethwd u:object_r:pawlethwd_exec:s0
/data/pawlet(/.*)? u:object_r:pawlet_hw_data_file:s0
+46
View File
@@ -0,0 +1,46 @@
# Copyright (C) 2026 oxmc / PawletOS
# SPDX-License-Identifier: Apache-2.0
# PawletOS hardware update daemon: detects peripherals, downloads firmware, and
# flashes them via authorized handler plugins. Runtime SELinux is permissive on
# PawletOS, but this policy is written to pass build-time neverallow checks.
type pawlethwd, domain, coredomain;
type pawlethwd_exec, exec_type, system_file_type, file_type;
init_daemon_domain(pawlethwd)
# Binder service.
type pawlet_hwupdate_service, service_manager_type;
binder_use(pawlethwd)
add_service(pawlethwd, pawlet_hwupdate_service)
allow pawlethwd servicemanager:service_manager list;
# Persistent state under /data/pawlet (created by init; see init.pawlethwd.rc).
# init labels /data/pawlet via file_contexts; files the daemon creates under it
# inherit pawlet_hw_data_file by default.
type pawlet_hw_data_file, file_type, data_file_type, core_data_file_type;
allow pawlethwd pawlet_hw_data_file:dir create_dir_perms;
allow pawlethwd pawlet_hw_data_file:file create_file_perms;
# Network access to download firmware from the update server.
net_domain(pawlethwd)
# Read the image-local manifest and dlopen handler plugins from /system_ext.
allow pawlethwd system_file:dir r_dir_perms;
allow pawlethwd system_file:file { r_file_perms execute map };
# USB enumeration (sysfs) + device access for detection and flashing.
allow pawlethwd sysfs:dir r_dir_perms;
allow pawlethwd sysfs:file r_file_perms;
allow pawlethwd usb_device:dir r_dir_perms;
allow pawlethwd usb_device:chr_file rw_file_perms;
# Read system properties.
get_prop(pawlethwd, default_prop)
# Clients that may look up the service (daemon-only surface for now; system
# apps/Settings can be added later).
allow system_server pawlet_hwupdate_service:service_manager find;
userdebug_or_eng(`
allow shell pawlet_hwupdate_service:service_manager find;
')
+4
View File
@@ -0,0 +1,4 @@
# Copyright (C) 2026 oxmc / PawletOS
# SPDX-License-Identifier: Apache-2.0
pawlet_hwupdate u:object_r:pawlet_hwupdate_service:s0