vpnhide/lsposed/native
okhsunrog c63c12bf86 fix(rust): tighten netlink/recvmsg edge cases + install rollback
Four small touches across zygisk and lsposed/native, all on adversarial-
or kernel-edge-case paths. Happy paths and the per-app filter behaviour
are unchanged.

zygisk hooks: recvmsg multi-iov no longer bypasses the filter
  Old code bailed out as soon as `msg_iovlen != 1`. A caller could
  hand recvmsg an iov array with one extra zero-length iov to skip
  filtering entirely. Now we filter the portion of `ret` that landed
  in iov[0] (recvmsg fills iovs in order) and propagate any shrink
  back into the returned byte count. Bytes that fell into iov[1..]
  pass through — bionic only ever uses iov[0], so legitimate dumps
  are unaffected; an attacker who deliberately splits a netlink
  message across iovs at least loses everything that landed in
  iov[0], which is strictly better than the previous bypass.

zygisk install_hooks: roll back partial installs on error
  install_hooks() walks five libc symbols and `?`-propagates the
  first failure. Previously, if hook 1-4 succeeded and hook 5 failed,
  the process kept hooks 1-4 in place — the app would see filtered
  ioctl/getifaddrs/openat/recvmsg but unfiltered recv, a torn plan
  that's worse than no install at all. Now we collect each
  shadowhook stub as it installs and, on the first failure, walk
  back through them in LIFO order calling shadowhook_unhook before
  surfacing the original error. Added a thin `shadowhook::unhook`
  wrapper so the FFI call site stays out of lib.rs.

zygisk hooks: document the TOCTOU window in is_dirfd_proc_net
  match_rel_proc_net resolves dirfd via readlink before the open,
  so a caller who races dup2/fchdir between the two can pass the
  classifier and have the open land elsewhere. This is accepted
  exposure (caller-controlled self-DoS, real detectors don't go
  through dirfd anyway) — added a comment so the next reader
  doesn't think it's an oversight.

lsposed/native for_each_rtattr: pass bounds-checked payload slice
  Old callbacks computed `b.as_ptr().add(rta_off + 4)` and read
  payload from there. If a kernel response set rta_len == 4
  (header only, zero payload), the read would pass the rtattr
  loop check (`if rta.rta_len < 4 { break }`) and then access
  bytes belonging to the next attr or past the message end. Now
  for_each_rtattr verifies `off + rta_len <= end` and yields a
  `&[u8]` payload slice; callbacks check its length before
  reading (e.g. `payload.len() >= 4` for an i32 ifindex).
  Replaces the `*const i32` read with a safe `i32::from_ne_bytes`.

Verified on Pixel 8 Pro (husky, android14-6.1, Android 16):
  Enforcing  : 26/26 PASS, COLD start ~1.9 s.
  Permissive : 22/26 PASS — same four by-design FAILs as before
               (proc_dev / sys_class_net / proc_ipv6_route /
               proc_if_inet6). No regression in netlink_getlink,
               netlink_getroute, getifaddrs, ioctl_*, proc_route,
               proc_fib_trie. zygisk and APK-native md5 match local
               build artifacts.
2026-04-27 01:54:47 +03:00
..
src fix(rust): tighten netlink/recvmsg edge cases + install rollback 2026-04-27 01:54:47 +03:00
build.rs build: align native cdylib on 16 KiB; unify kmod/zygisk build scripts 2026-04-26 23:26:30 +03:00
Cargo.lock refactor(lsposed): polish Gobley/UniFFI migration 2026-04-26 04:41:24 +03:00
Cargo.toml refactor(lsposed): polish Gobley/UniFFI migration 2026-04-26 04:41:24 +03:00