Skip to content

Bump c-ares 1.19.1 to 1.34.6 - #85

Merged
glbrntt merged 7 commits into
apple:mainfrom
iko1:bump-c-ares-1.34.6
Jul 16, 2026
Merged

glbrntt merged 7 commits into
apple:mainfrom
iko1:bump-c-ares-1.34.6

Conversation

@iko1

@iko1 iko1 commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Updates the vendored c-ares C library from 1.19.1 to 1.34.6 (latest stable), picking up ~15 minor releases of bug fixes, security hardening, and protocol support.

Changes

Submodule

  • Sources/CAsyncDNSResolver/c-ares → v1.34.6

Build integration (Package.swift)

  • c-ares 1.22+ split src/lib into subdirectories (record/, str/, util/, dsa/, event/, legacy/, thirdparty/, and an internal src/lib/include/). SwiftPM globs sources recursively, so this needed:
    • Added ./c-ares/src/lib/include to the header search path.
    • Extended the exclude list for the two new non-source files.

Config headers (cross-platform)

  • ares_config.h / ares_build.h regenerated for 1.34.6 (hand-vendored, since SwiftPM can't run c-ares's cmake step), keeping the repo's existing #if defined(_WIN32) → config-win32.h split.
  • The regenerated config was produced on macOS, which baked in Darwin-/SDK-specific answers that broke other platforms. Neutralized those in the shared POSIX section so one config builds everywhere, matching main's lowest-common-denominator convention. Concretely this fixes:
    • AvailabilityMacros.h not found on Linux (ares_rand.c).
    • memmem undeclared on Linux/glibc (ares_str.c) — falls back to c-ares's portable implementation.
    • sys/random.h not found on newer macOS SDKs — falls back to /dev/urandom.

ares_status_t migration

c-ares 1.20+ moved the status constants from int #defines into a real ares_status_t enum (a distinct Swift type). Bridged via an ares_status_t(_ status: Int32) initializer so the readable case ARES_SUCCESS: labels stay intact; behavior unchanged.

Deprecated-API migration (DNSResolver_c-ares.swift, AresChannel.swift)

1.34 deprecates ares_query, ares_getsock, and all ares_parse_*_reply functions. Migrated to the modern API:

  • Queries: ares_query → ares_query_dnsrec. Its callback delivers an already-parsed ares_dns_record_t, so the 10 reply parsers now read fields via the ares_dns_record_rr_* / ares_dns_rr_get_* accessors instead of ares_parse_*_reply. Each parser filters the answer section by record type (the old API filtered internally). This also removes per-parser buffer allocation and a pre-existing hostent/data leak.
  • Synchronous status: ares_query_dnsrec reports a status; on failure the callback doesn't fire, so the handler is freed and the continuation failed at the call site.
  • Socket processing: ares_getsock → ARES_OPT_SOCK_STATE_CB. AresChannel owns a SocketRegistry that the socket-state callback keeps current; QueryProcessor polls it to drive ares_process_fd. The registry's lifetime is bracketed by the channel so it stays valid across ares_destroy.
  • Cancellation (onCancel) still uses ares_cancel, which is not deprecated.

@glbrntt glbrntt added the 🔨 semver/patch No public API change. label Jun 30, 2026

@glbrntt glbrntt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for opening this, unfortunately it doesn't build on Linux though so you'll need to figure those build issues out.

@iko1

iko1 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for opening this, unfortunately it doesn't build on Linux though so you'll need to figure those build issues out.

Thanks! Fixed. The ares_config.h had been regenerated on macOS, so it carried Darwin-only defines into the shared header, which broke Linux. I've removed those from the POSIX branch to match the cross-platform approach already used on main, and merged latest main to resolve the conflict.

Verified on macOS and Ubuntu 22.04 / Swift 6.1.

@iko1
iko1 requested a review from glbrntt July 7, 2026 08:29

@glbrntt glbrntt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that your other PR has been merged you'll need to update this PR to incorporate those changes.

@iko1

iko1 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Now that your other PR has been merged you'll need to update this PR to incorporate those changes.

@glbrntt , Done, I've merged main into this branch and resolved the conflicts.

@iko1
iko1 requested a review from glbrntt July 14, 2026 17:36
@iko1
iko1 requested a review from glbrntt July 16, 2026 05:51

@glbrntt glbrntt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@glbrntt
glbrntt merged commit 1bd427a into apple:main Jul 16, 2026
42 checks passed
josephnoir pushed a commit that referenced this pull request Oct 30, 2026
### Motivation

  Fixes #90.

A query that fails synchronously crashes instead of throwing. Our iOS
app hit this three times across two devices (iOS 26.4.2 and 26.7.1):
twice as SIGSEGV in swift_release at DNSResolver_c-ares.swift:209, once
as a libmalloc invalid-free abort at :210. All three had the same stack
through Ares.query, and all occurred when a device lost its route to the
configured resolvers.

`Ares.query` frees the `QueryReplyHandler` box when `ares_query_dnsrec`
returns a non-success status, on the premise
stated in the comment: *"On failure the callback will not fire."* That
premise doesn't hold — c-ares invokes the
callback **and** returns the error on every reachable synchronous
failure path:

  ```c
  if (ares_slist_len(channel->servers) == 0) {
      callback(arg, ARES_ENOSERVER, 0, NULL);   // ares_send.c:123
      return ARES_ENOSERVER;                    // ares_send.c:124
  }
  ```

Same shape at `ares_send.c:141-142`, `:162-163` and `:172-174`. The
callback's own `defer`
(`DNSResolver_c-ares.swift:184-187`) has already released the handler
and deallocated the box, so the cleanup on the
  error return is a second release of freed memory.

This is a regression in 0.8.0 (`1bd427a`, #85), which introduced the
block as part of moving from the deprecated
`ares_query` to `ares_query_dnsrec`. The void-returning `ares_query` had
no synchronous status to misinterpret.

  ### Modifications

Removed the `deinitialize`/`deallocate` pair from the `status !=
ARES_SUCCESS` branch and corrected the comment.

`handler.handle(status:dnsrec:)` is deliberately kept.
`ares_query_dnsrec` returns `ARES_EFORMERR` without calling the
callback when the channel is null (`ares_query.c:123-125`), and that is
the only path where this call is what resumes
the continuation — removing it would turn the crash into a hang. On the
paths that do call back it is a no-op, since
`QueryReplyHandler` enforces resume-once by clearing `resume` to `nil`
under its lock.

  Added `CAresSynchronousFailureTests`.

  ### Result

A synchronous failure from `ares_query_dnsrec` surfaces as a thrown
`AsyncDNSResolver.Error` instead of terminating the
  process. No change to the success path or to asynchronous failures.

  ### Test Plan

The new test reaches a synchronous failure offline, with no network,
routing changes or privileges. An empty server list
is joined into an empty CSV and passed to `ares_set_servers_ports_csv`,
which treats it as "blank all servers" and
succeeds (`ares_update_servers.c:1280-1286`), leaving the channel with
none — so every subsequent query takes the
`ares_slist_len(channel->servers) == 0` path above. It repeats 50 times,
because a single over-release can land on
  memory that hasn't been reused and pass unnoticed.

  Without the patch, the test target aborts on the first test:

  ```
  Test Case '-[AsyncDNSResolverTests.CAresSynchronousFailureTests
test_synchronousFailure_throwsAndDoesNotDoubleFreeTheQueryHandler]'
started.
  error: Process '.../xctest ...' exited with unexpected signal code 6
  ```

Signal 6 is libmalloc's invalid-free check on the second `deallocate`.
With the patch, both tests pass.

Two of the three reported crashes took the other route — SIGSEGV in
swift_release — because the freed box had already
been reused, so the first deinitialize read a recycled pointer and
followed it. The third was the same abort this test
   produces. Which symptom appears depends only on allocator timing.

Our app worked around this by switching to the dnssd backend, so the
patched c-ares path hasn't been exercised in production — the evidence
for the fix itself is the test above.

---------

Co-authored-by: EnzoHerrera <enzoherrera@cattlekrush.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants