Skip to content

unix: Improve support for FreeBSD.#18979

Open
agatti wants to merge 5 commits into
micropython:masterfrom
agatti:freebsd-fixes
Open

unix: Improve support for FreeBSD.#18979
agatti wants to merge 5 commits into
micropython:masterfrom
agatti:freebsd-fixes

Conversation

@agatti

@agatti agatti commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR addresses a few minor issues related to MicroPython on the FreeBSD platform:

  • FFI tests tried to open a now missing libc library version (libc.so.6 is no longer there, now it's called libc.so.7)
  • The system-wide entry in sys.path for the standard library pointed to /usr/lib/micropyhon, but on FreeBSD anything outside the base system files has to reside elsewhere; the usual root under /usr for third party packages is /usr/local
  • sys.platform now reports it's running on FreeBSD instead of Linux
  • The interpreter banner recognises FreeBSD as a separate platform and sets the correct libc name
  • platform.platform now reports FreeBSD as the operating system
  • The EDEADLK result code returned on failed lock acquisition is handled, aliased to EBUSY at the moment
  • The extmod/select_poll_fd test is skipped on FreeBSD

This is technically the continuation of micropython/micropython-lib#1099, as it fixes FFI support on recent FreeBSD versions in micropython/micropython-lib.

Testing

The test suite was executed in a FreeBSD 15.0-RELEASE x64 virtual machine, with no FFI-related test failures. thread/thread_heap_lock, and thread/thread_lock5 were failing before these changes as well. I haven't yet looked into those two tests.

Trade-offs and Alternatives

The changes in this PR should affect MicroPython's behaviour exclusively when built on FreeBSD. FFI and sys.path changes might also apply to OpenBSD, NetBSD, and their derivatives, but they weren't tested on those OSes.

Generative AI

I did not use generative AI tools when creating this PR.

agatti added 4 commits March 22, 2026 21:58
This commit makes two failing FFI tests pass on FreeBSD 15.0-RELEASE.

Those tests explicitly looked for the system's libc and libm libraries
by versioned file names, both of which do not have matches on current
FreeBSD installations.  The new versioned file names have been added to
make the tests pass.

Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
This commit fixes the default system libraries path on FreeBSD, as its
filesystem conventions differ from regular Linux (which seems to be the
default chosen here).

On FreeBSD "/usr/lib" is used exclusively by base system packages, and
anything else is supposed to either use "/usr/local/lib" or something
else under "/opt".  Now builds for FreeBSD will use a sensible default
system path, which should also easen the burden of whoever will update
the FreeBSD ports' version of MicroPython.

This in theory should work on other BSD systems as well but that
assumption was not tested.

Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
This commit lets the "sys" module report the correct value of
"sys.platform" on FreeBSD systems.

The only two platforms known to the module were Darwin and Linux, with
the latter used as a fallback.  Now FreeBSD is also recognised as such,
with the appropriate string being set.

Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
This commit lets the platform module recognise FreeBSD as its own thing.

A new entry in the libc is added, simply called "libc" (I didn't find
any more descriptive name out there), and the platform is reported as
"FreeBSD" when applicable.

Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
@github-actions

github-actions Bot commented Mar 22, 2026

Copy link
Copy Markdown

Code size report:

Reference:  mimxrt: Fix flash erase operations on Hyperflash boards. [1f50d05]
Comparison: tests/extmod/select_poll_fd.py: Skip test on FreeBSD. [merge of 9ea008a]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@codecov

codecov Bot commented Mar 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.47%. Comparing base (5c00edc) to head (9ea008a).
⚠️ Report is 361 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #18979   +/-   ##
=======================================
  Coverage   98.46%   98.47%           
=======================================
  Files         176      176           
  Lines       22784    22845   +61     
=======================================
+ Hits        22435    22497   +62     
+ Misses        349      348    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dpgeorge dpgeorge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, it's a nice improvement.

Just a few comments.

Comment thread ports/unix/mpthreadport.c Outdated
}
#if defined(__FreeBSD__)
else if (ret == EDEADLK) {
return 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe this case should be an error (and hence raise an exception on the Python side)? Because if you ask to wait for the lock and don't get it... that's unexpected.

@agatti agatti Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As far as I know, this occurs whenever a thread attempts to wait on a lock that it already owns: https://github.com/freebsd/freebsd-src/blob/62c1865c9aaef436498c444b460e6ec2fbcaf44d/sys/kern/kern_lock.c#L614 (and https://github.com/freebsd/freebsd-src/blob/62c1865c9aaef436498c444b460e6ec2fbcaf44d/sys/kern/kern_lockf.c#L893 although there are more instances in that file).

However I see that mutexes are initialised with PTHREAD_MUTEX_RECURSIVE so, assuming this flag is actually taken into account, my only guess is that the way threads are handled in MicroPython triggers a potential cycle in FreeBSD's lock graph. One may assume that it's a transient situation, hence the aliasing that to Linux's EBUSY case (or possibly treat it as if it returned EAGAIN).

Removing this particular bit may also require more changes since EDEADLK isn't part of py/mperrno.h and has to be added there to handle the situation cleanly in the affected tests. My main concern with this is that EDEADLK on Linux comes from pthreads, so in theory it may not be there. Also, I'm not entirely sure if aliasing EDEADLK to EAGAIN at the errno level is the correct thing to do w.r.t. threads since on non-Linux EAGAIN may mean something slightly different in other cases.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I just installed FreeBSD 15.0-RELEASE-p9 in a VM, and built the unix port (standard variant) from master (ie not this PR) with clang (I disabled BTREE, FFI, SSL options). It runs the tests/threads/thread_coop.py test just fine, whereas the commit message here says the EDEADLK code is needed to pass that test.

All the thread tests pass for me on master, except the two you noted: thread_heap_lock.py and thread_lock5.py. And they fail exactly due to raising EDEADLK! Those tests do test the main thread re-acquiring a lock, but a separate thread is supposed to release it. Maybe FreeBSD doesn't support this use case?? Although that would be very surprising.

There's something here that needs fixing. The thread_heap_lock.py and thread_lock5.py tests are giving a big clue, and ideally we would get them passing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, so mutexes associated with _thread.allocate_lock() are not initialize recursively. So that's why FreeBSD is returning EDEADLK, because the thread tries to acquire it a second time (in those two tests that fail).

This works OK on Linux (at least) but obviously not FreeBSD's implementation of pthreads.

Making it recursive won't work, it's supposed to block the main thread ("deadlock" it actually) until another thread releases the lock.

Maybe we need to use semaphores instead to implement thread.allocate_lock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd be surprised too if releasing a mutex from a different thread isn't possible on FreeBSD.

I wish I remember which patch level I used when drafting this PR, but I'm quite sure thread_coop.py failed on me because EDEADLK was returned, I seem to recall it happened when the two threads' execution models were swapped. It could have been that it took longer than expected to shutdown the thread on FreeBSD, but given the situation a retry condition made sense.

Still, yes, there's more to look at! I'll re-create the VM and look at this too. Thanks for trying this out on your end as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, mystery solved! Here's the relevant part of CPython's _thread implementation (Python/thread_pthread.h):

/* A pthread mutex isn't sufficient to model the Python lock type
 * because, according to Draft 5 of the docs (P1003.4a/D5), both of the
 * following are undefined:
 *  -> a thread tries to lock a mutex it already has locked
 *  -> a thread tries to unlock a mutex locked by a different thread
 * pthread mutexes are designed for serializing threads over short pieces
 * of code anyway, so wouldn't be an appropriate implementation of
 * Python's locks regardless.
 *
 * The pthread_lock struct implements a Python lock as a "locked?" bit
 * and a <condition, mutex> pair.  In general, if the bit can be acquired
 * instantly, it is, else the pair is used to block the thread until the
 * bit is cleared.     9 May 1994 tim@ksr.com
 */

typedef struct {
    char             locked; /* 0=unlocked, 1=locked */
    /* a <cond, mutex> pair to handle an acquire of a locked lock */
    pthread_cond_t   lock_released;
    pthread_mutex_t  mut;
} pthread_lock;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Interesting. Looks like the situation is specified to be ambiguous too! From https://pubs.opengroup.org/onlinepubs/007908799/xsh/pthread_mutex_lock.html:

If the mutex type is PTHREAD_MUTEX_RECURSIVE, then the mutex maintains the concept of a lock count. When a thread successfully acquires a mutex for the first time, the lock count is set to one. Every time a thread relocks this mutex, the lock count is incremented by one. Each time the thread unlocks the mutex, the lock count is decremented by one. When the lock count reaches zero, the mutex becomes available for other threads to acquire. If a thread attempts to unlock a mutex that it has not locked or a mutex which is unlocked, an error will be returned.
[...]
The pthread_mutex_lock() function may fail if:

[EDEADLK]
The current thread already owns the mutex.
[...]
The pthread_mutex_unlock() function may fail if:

[EPERM]
The current thread does not own the mutex.

So, recursive mutexes are specified but may not work, and threads may or may not unlock other threads' mutexes...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's quite a bit of refactoring to fix this properly.

For this PR I suggest just undo the changes to this file and accept the thread test failures under FreeBSD for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I've updated tests/extmod/select_poll_fd.py as well.

raise SystemExit
except (ImportError, AttributeError):
print("SKIP")
raise SystemExit

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Having the sys module is a requirement for a port to be able to run the tests, but sys.platform can be missing.

If a port has the select and errno modules then it's highly likely it has sys.platform. So I suggest putting the check after the import of select/errno, and just access sys.platform without any try/except, like this:

if sys.platform == "freebsd":
    print("SKIP")
    raise SystemExit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Turns out sys may not be there, after all. This test is also checked against CPython, which fails with an import error (eg. https://github.com/micropython/micropython/actions/runs/27251624169/job/80477170312?pr=18979).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, sorry, I meant that a port is required to implement the sys module (I didn't mean that sys will already be imported).

Anyway, what you have here is good!

This commit makes the `extmod/select_poll_fd` test get skipped on
FreeBSD.

The test depends on the maximum number of available file descriptors
being less than 6000, which is not the case on FreeBSD (it's ~120k).
Right now there's no way to query the value directly or an easy way to
get the output of `os.system("ulimit -n")`, so the test is skipped on
FreeBSD.

Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants