diff --git a/config/common.mk b/config/common.mk index d0c2122..fa49cbb 100644 --- a/config/common.mk +++ b/config/common.mk @@ -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) # --------------------------------------------------------------------------- diff --git a/hwupdate/Android.bp b/hwupdate/Android.bp new file mode 100644 index 0000000..e7aef81 --- /dev/null +++ b/hwupdate/Android.bp @@ -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, +} diff --git a/hwupdate/README.md b/hwupdate/README.md new file mode 100644 index 0000000..0873336 --- /dev/null +++ b/hwupdate/README.md @@ -0,0 +1,84 @@ + +# 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_.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_.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. diff --git a/hwupdate/aidl/Android.bp b/hwupdate/aidl/Android.bp new file mode 100644 index 0000000..bcf696b --- /dev/null +++ b/hwupdate/aidl/Android.bp @@ -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, + }, + }, +} diff --git a/hwupdate/aidl/me/pawlet/hardware/update/HwComponent.aidl b/hwupdate/aidl/me/pawlet/hardware/update/HwComponent.aidl new file mode 100644 index 0000000..acd6f7e --- /dev/null +++ b/hwupdate/aidl/me/pawlet/hardware/update/HwComponent.aidl @@ -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; +} diff --git a/hwupdate/aidl/me/pawlet/hardware/update/HwUpdateInfo.aidl b/hwupdate/aidl/me/pawlet/hardware/update/HwUpdateInfo.aidl new file mode 100644 index 0000000..8476632 --- /dev/null +++ b/hwupdate/aidl/me/pawlet/hardware/update/HwUpdateInfo.aidl @@ -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; +} diff --git a/hwupdate/aidl/me/pawlet/hardware/update/IPawletHardwareUpdate.aidl b/hwupdate/aidl/me/pawlet/hardware/update/IPawletHardwareUpdate.aidl new file mode 100644 index 0000000..e0bb305 --- /dev/null +++ b/hwupdate/aidl/me/pawlet/hardware/update/IPawletHardwareUpdate.aidl @@ -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(); +} diff --git a/hwupdate/daemon/Android.bp b/hwupdate/daemon/Android.bp new file mode 100644 index 0000000..ec490fe --- /dev/null +++ b/hwupdate/daemon/Android.bp @@ -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"], +} diff --git a/hwupdate/daemon/catalog.cpp b/hwupdate/daemon/catalog.cpp new file mode 100644 index 0000000..b2d55dd --- /dev/null +++ b/hwupdate/daemon/catalog.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "catalog.h" + +#include +#include + +#include + +#include + +#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(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 << "¤t_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 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 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 diff --git a/hwupdate/daemon/catalog.h b/hwupdate/daemon/catalog.h new file mode 100644 index 0000000..628c96a --- /dev/null +++ b/hwupdate/daemon/catalog.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#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 CheckUpdate(const std::string& baseUrl, const CheckRequest& req); + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/detect.cpp b/hwupdate/daemon/detect.cpp new file mode 100644 index 0000000..9fab437 --- /dev/null +++ b/hwupdate/daemon/detect.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "detect.h" + +#include +#include +#include + +#include +#include +#include + +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 EnumUsb() { + std::vector 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& 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 Detect(const Manifest& manifest) { + std::vector usb = EnumUsb(); + std::vector out; + + for (const auto& prod : manifest.products) { + for (const auto& comp : prod.components) { + DetectedComponent dc; + dc.product = ∏ + dc.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 diff --git a/hwupdate/daemon/detect.h b/hwupdate/daemon/detect.h new file mode 100644 index 0000000..50d76ec --- /dev/null +++ b/hwupdate/daemon/detect.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#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 Detect(const Manifest& manifest); + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/engine.cpp b/hwupdate/daemon/engine.cpp new file mode 100644 index 0000000..401d5eb --- /dev/null +++ b/hwupdate/daemon/engine.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "engine.h" + +#include +#include + +#include + +#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& 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(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 lk(reqMu_); + if (stop_) return; + stop_ = true; + reqCv_.notify_all(); + } + if (worker_.joinable()) worker_.join(); +} + +void Engine::CheckNow() { + std::lock_guard lk(reqMu_); + requested_ = true; + reqCv_.notify_all(); +} + +void Engine::WorkerLoop() { + for (;;) { + { + std::unique_lock 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 detected = Detect(*manifest); + + std::vector comps; + std::vector 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(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 lk(snapMu_); + components_ = std::move(comps); + pending_ = std::move(pend); + } + return 0; +} + +std::vector Engine::Components() const { + std::lock_guard lk(snapMu_); + return components_; +} + +std::vector Engine::Pending() const { + std::lock_guard lk(snapMu_); + return pending_; +} + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/engine.h b/hwupdate/daemon/engine.h new file mode 100644 index 0000000..0447913 --- /dev/null +++ b/hwupdate/daemon/engine.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#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 Components() const; + std::vector 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 components_; + std::vector pending_; + + std::thread worker_; + std::mutex reqMu_; + std::condition_variable reqCv_; + bool requested_ = false; + bool stop_ = false; + std::atomic busy_{false}; + std::atomic lastResult_{0}; +}; + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/handlers.cpp b/hwupdate/daemon/handlers.cpp new file mode 100644 index 0000000..b447e94 --- /dev/null +++ b/hwupdate/daemon/handlers.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "handlers.h" + +#include + +#include + +#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(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 diff --git a/hwupdate/daemon/handlers.h b/hwupdate/daemon/handlers.h new file mode 100644 index 0000000..766fb2c --- /dev/null +++ b/hwupdate/daemon/handlers.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#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_.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 cache_; +}; + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/http.cpp b/hwupdate/daemon/http.cpp new file mode 100644 index 0000000..f245973 --- /dev/null +++ b/hwupdate/daemon/http.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "http.h" + +#include + +#include + +#include + +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(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(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 diff --git a/hwupdate/daemon/http.h b/hwupdate/daemon/http.h new file mode 100644 index 0000000..0ab71d1 --- /dev/null +++ b/hwupdate/daemon/http.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +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 diff --git a/hwupdate/daemon/init.pawlethwd.rc b/hwupdate/daemon/init.pawlethwd.rc new file mode 100644 index 0000000..a1b1d38 --- /dev/null +++ b/hwupdate/daemon/init.pawlethwd.rc @@ -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. diff --git a/hwupdate/daemon/main.cpp b/hwupdate/daemon/main.cpp new file mode 100644 index 0000000..6e7514b --- /dev/null +++ b/hwupdate/daemon/main.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include +#include +#include +#include + +#include + +#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 service = android::sp::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; +} diff --git a/hwupdate/daemon/manifest.cpp b/hwupdate/daemon/manifest.cpp new file mode 100644 index 0000000..d1c1806 --- /dev/null +++ b/hwupdate/daemon/manifest.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "manifest.h" + +#include + +#include + +#include + +namespace pawlet::hwupdate { +namespace { + +std::vector StrArray(const Json::Value& v) { + std::vector 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& 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 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 diff --git a/hwupdate/daemon/manifest.h b/hwupdate/daemon/manifest.h new file mode 100644 index 0000000..6c148c5 --- /dev/null +++ b/hwupdate/daemon/manifest.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#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 LoadManifest(const std::string& path); + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/service.cpp b/hwupdate/daemon/service.cpp new file mode 100644 index 0000000..513a759 --- /dev/null +++ b/hwupdate/daemon/service.cpp @@ -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* 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* 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 diff --git a/hwupdate/daemon/service.h b/hwupdate/daemon/service.h new file mode 100644 index 0000000..b9e6149 --- /dev/null +++ b/hwupdate/daemon/service.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#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 diff --git a/hwupdate/daemon/state.cpp b/hwupdate/daemon/state.cpp new file mode 100644 index 0000000..d376852 --- /dev/null +++ b/hwupdate/daemon/state.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "state.h" + +#include + +#include + +#include + +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 diff --git a/hwupdate/daemon/state.h b/hwupdate/daemon/state.h new file mode 100644 index 0000000..b899c96 --- /dev/null +++ b/hwupdate/daemon/state.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +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 entries_; +}; + +} // namespace pawlet::hwupdate diff --git a/hwupdate/daemon/types.h b/hwupdate/daemon/types.h new file mode 100644 index 0000000..b47652b --- /dev/null +++ b/hwupdate/daemon/types.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include + +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 supportedRevisions; + std::vector installerHandlers; + // detection identifiers (optional; may also come from the catalog) + std::string usbVendorId; + std::string usbProductId; + std::vector dtCompatible; +}; + +struct ManifestProduct { + std::string id; + std::string manufacturer; + std::string cls; + std::string support; + std::vector supportedRevisions; + std::string usbVendorId; + std::string usbProductId; + std::vector dtCompatible; + std::vector components; +}; + +struct Manifest { + int schemaVersion = 0; + std::string device; + std::string buildVersion; + std::vector 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 diff --git a/hwupdate/daemon/util.cpp b/hwupdate/daemon/util.cpp new file mode 100644 index 0000000..a154d2e --- /dev/null +++ b/hwupdate/daemon/util.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#include "util.h" + +#include + +#include +#include +#include + +#include + +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 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(a[i])) != + std::tolower(static_cast(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 diff --git a/hwupdate/daemon/util.h b/hwupdate/daemon/util.h new file mode 100644 index 0000000..db06eca --- /dev/null +++ b/hwupdate/daemon/util.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2026 oxmc / PawletOS + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +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 diff --git a/hwupdate/etc/supported_hardware.default.json b/hwupdate/etc/supported_hardware.default.json new file mode 100644 index 0000000..f509529 --- /dev/null +++ b/hwupdate/etc/supported_hardware.default.json @@ -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" + } + } + ] + } + ] +} diff --git a/hwupdate/handlers/usbdfu/Android.bp b/hwupdate/handlers/usbdfu/Android.bp new file mode 100644 index 0000000..405b33a --- /dev/null +++ b/hwupdate/handlers/usbdfu/Android.bp @@ -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", + ], +} diff --git a/hwupdate/handlers/usbdfu/usbdfu.cpp b/hwupdate/handlers/usbdfu/usbdfu.cpp new file mode 100644 index 0000000..373895b --- /dev/null +++ b/hwupdate/handlers/usbdfu/usbdfu.cpp @@ -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 +#include + +#include +#include +#include +#include +#include +#include + +#include + +#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(strtoul(s, nullptr, 16)); +} + +bool ReadFile(const char* path, std::vector* 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(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 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(std::min(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(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(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; } diff --git a/hwupdate/hwupdate.mk b/hwupdate/hwupdate.mk new file mode 100644 index 0000000..181e9be --- /dev/null +++ b/hwupdate/hwupdate.mk @@ -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 diff --git a/hwupdate/include/pawlet/hwh_abi.h b/hwupdate/include/pawlet/hwh_abi.h new file mode 100644 index 0000000..9058e5e --- /dev/null +++ b/hwupdate/include/pawlet/hwh_abi.h @@ -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_.so + * that exports exactly one symbol: + * + * const pawlet_hwh_api* pawlet_hwh_entry(void); + * + * The daemon dlopen()s the library only when 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 +#include + +#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 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 */ diff --git a/hwupdate/schemas/device-updates.schema.json b/hwupdate/schemas/device-updates.schema.json new file mode 100644 index 0000000..3d53df2 --- /dev/null +++ b/hwupdate/schemas/device-updates.schema.json @@ -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 } + } + } + } + } + } +} diff --git a/hwupdate/schemas/supported-hardware.schema.json b/hwupdate/schemas/supported-hardware.schema.json new file mode 100644 index 0000000..07e7a3a --- /dev/null +++ b/hwupdate/schemas/supported-hardware.schema.json @@ -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 + } + } +} diff --git a/hwupdate/sepolicy/file_contexts b/hwupdate/sepolicy/file_contexts new file mode 100644 index 0000000..9c40599 --- /dev/null +++ b/hwupdate/sepolicy/file_contexts @@ -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 diff --git a/hwupdate/sepolicy/pawlethwd.te b/hwupdate/sepolicy/pawlethwd.te new file mode 100644 index 0000000..f218a5e --- /dev/null +++ b/hwupdate/sepolicy/pawlethwd.te @@ -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; +') diff --git a/hwupdate/sepolicy/service_contexts b/hwupdate/sepolicy/service_contexts new file mode 100644 index 0000000..ccd829d --- /dev/null +++ b/hwupdate/sepolicy/service_contexts @@ -0,0 +1,4 @@ +# Copyright (C) 2026 oxmc / PawletOS +# SPDX-License-Identifier: Apache-2.0 + +pawlet_hwupdate u:object_r:pawlet_hwupdate_service:s0