Skip to content

py/persistentcode: always call reader->close() from mp_raw_code_load() - #3874

Closed
tomlogic wants to merge 3 commits into
micropython:masterfrom
tomlogic:bugfix/close-reader-on-mp_raw_code_load-error
Closed

tomlogic wants to merge 3 commits into
micropython:masterfrom
tomlogic:bugfix/close-reader-on-mp_raw_code_load-error

Conversation

@tomlogic

Copy link
Copy Markdown
Contributor

Somewhat related to #3843. If we run into an error trying to import a module stored as a .mpy file, we never call reader->close() to release the open file handle.

This PR adds exception handling to mp_raw_code_load() and has it close the reader before re-throwing the exception.

@tomlogic

Copy link
Copy Markdown
Contributor Author

I decided to include a patch to resolve #3843 as well. Open to ideas on ways to improve this, possibly with less code?

@dpgeorge

Copy link
Copy Markdown
Member

Also kind of related is #3478.

@dpgeorge

Copy link
Copy Markdown
Member

Open to ideas on ways to improve this, possibly with less code?

One option is to refactor parsing and loading of .mpy files so they don't raise exceptions, but instead return error codes. But this is difficult because this code relies on some object constructors (eg makeing new strings, ints, floats, complex's) and all these constructors would need an alternative version that didn't raise an exception.

@dpgeorge

Copy link
Copy Markdown
Member

We are somehow going to need to add tests that test the invalid-mpy parts of the code.

@dpgeorge

Copy link
Copy Markdown
Member

We are somehow going to need to add tests that test the invalid-mpy parts of the code.

Added in 6d8816f

@pfalcon

pfalcon commented Jun 18, 2018

Copy link
Copy Markdown
Contributor

There's a well-known pattern of handling exceptions - at the "top level" (instead of many intermediate levels), and that's what MicroPython follows pretty much. For example, all calls mp_parse() are already wrapped in nlr_push(), and if something needs to be done about its lex param, that would be the place, instead of bloating up the stack of intermediate functions.

@pfalcon

pfalcon commented Jun 18, 2018

Copy link
Copy Markdown
Contributor

Also note that object deallocation in MicroPython is "best effort", not everything is freed/deallocated explicitly, because there's GC, which will take care of that. For handling non-memory resources, like open file descriptors, there's GC finalization support.

@tomlogic

Copy link
Copy Markdown
Contributor Author

I think it's safer to wrap the function with an exception handler, as I've done, but I'm wondering about some other idiom for that code. For example, what if we replace this:

    nlr_buf_t nlr;
    if (nlr_push(&nlr) == 0) {
        mp_parse_tree_t rc = _mp_parse(lex, input_kind);
        nlr_pop();

        // free the lexer on behalf of the caller before returning
        mp_lexer_free(lex);
        return rc;
    } else {
        // exception; free lexer and re-raise same exception
        mp_lexer_free(lex);
        nlr_jump(nlr.ret_val);
    }

with this:

    mp_parse_tree_t rc;
    nlr_buf_t nlr;
    if (nlr_push(&nlr) == 0) {
        rc = _mp_parse(lex, input_kind);
        nlr_pop();
    }
    // free the lexer on behalf of the caller before returning
    mp_lexer_free(lex);

    if (failed) {
        // re-raise exception
        nlr_jump(nlr.ret_val);
    }
    return rc;

I'm not sure what to substitute for the failed conditional at the end. I like this format as it eliminates duplicated code (call to mp_lexer_free()) which makes maintenance easier and potentially reduces code size (mp_parse_compile_execute() has duplicated calls to mp_globals_set() and mp_locals_set()). Do you make failed a boolean initialized at the start of the function and set/cleared after the nlr_pop()? Can we look at something in the nlr_buf_t structure to know whether it was successful or not? Maybe initialize nlr.ret_val to NULL (as part of nlr_push() to reduce duplicated code) and check that?

@tomlogic

Copy link
Copy Markdown
Contributor Author

And for reference, here's the test sequence I was using to validate my fix:

>>> import os
>>> with open("testmodule.mpy", "w") as testmodule:
...     testmodule.write("not valid")
... 
9
>>> import testmodule
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: incompatible .mpy file
>>> os.remove("testmodule.mpy")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 7001] EPERM 

And a version for the .py file just used '(((\r' for the file contents.

>>> import testmodule
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/flash/testmodule.py", line 2
SyntaxError: invalid syntax

@tomlogic

Copy link
Copy Markdown
Contributor Author

@dpgeorge Any thoughts on merging this code? I've held off on it in our MicroPython fork but may need to merge it so we're not leaving files open on a failed import.

@tomlogic

Copy link
Copy Markdown
Contributor Author

@dpgeorge We're planning to ship these changes in an upcoming firmware release. Wondering if it can get some consideration for upstream merge.

The Travis failure is just an (expected) increase in code size:

Old size: 71620 new size: 71656
Validation failure: Core code size increased

@pfalcon

pfalcon commented Oct 24, 2018

Copy link
Copy Markdown
Contributor

is just an (expected) increase in code size:

Well, not just the code size. As was explained above, this also increases the stack usage noticeably, and possibly recursively (explicit analysis of that should be persistent part of this change, i.e. be in the commit messages).

And I don't even mention coverage decrease (love "we can do it later" excuse myselef).

@pfalcon

pfalcon commented Oct 24, 2018

Copy link
Copy Markdown
Contributor

but I'm wondering about some other idiom for that code

Yes, there's another idiom: #4131

dhalbert added a commit to tannewt/circuitpython that referenced this pull request Dec 24, 2020
@dpgeorge

Copy link
Copy Markdown
Member

This is still a problem. Apart from the solution here (which uses a lot of stack), one way to tackle it is to make sure finalisers are used for all streams used by the parser/raw-code-loader, and then do a GC collect after importing (to make sure any non-freed streams are freed by the GC).

Alternatively, #4131 would be the best way to go.

@dpgeorge dpgeorge added the py-core Relates to py/ directory in source label Nov 30, 2021
@dpgeorge

dpgeorge commented Sep 5, 2023

Copy link
Copy Markdown
Member

I'll close this in favour of #12369, which uses much less C stack space to implement the same thing.

Also, the test here doesn't work on Linux (it passes even without the fix provided in this PR).

dpgeorge added a commit to dpgeorge/micropython that referenced this pull request Sep 14, 2023
@dpgeorge dpgeorge closed this Sep 14, 2023
jprodriguez-nbs pushed a commit to jprodriguez-nbs/micropython that referenced this pull request Sep 21, 2023
Taccart pushed a commit to Taccart/micropython that referenced this pull request May 23, 2026
UKTailwind added a commit to UKTailwind/micropython that referenced this pull request Sep 2, 2026
The PicoMite tree ran its TinyUSB 0.21 host work on this same hardware -
two keyboards (one marginal), a touch panel and a flash drive behind one
hub - across software, hardware and power-on resets, and wrote up what
actually held (docs/usb-host-hardening).  Root cause: the RP2 SIE keeps
ONE handshake-result latch shared between EPX and the interrupt-endpoint
poller (upstream micropython#3533), so an interrupt poll can overwrite a control
transfer's ACK before the IRQ handler reads it, and the transfer is
misread as RX_TIMEOUT.  The validated answer is to tolerate the race
above the silicon, not to adopt the strict micropython#3533 driver rewrite - which
enumerated FEWER devices on the marginal rig.  Applied here:

  hcd_rp2040.patch  Grace period for EP0 RX timeouts: within a 1 s
                    window the transfer is left armed and the clobbered
                    result outlasted; the window closes on any EP0
                    completion.  Expiry (a genuinely dead device) takes
                    the original fail path, which keeps our buffer clear
                    (micropython#3874).  Bulk/interrupt keep the fast-fail: their
                    flow control is NAK, which never raises RX_TIMEOUT.

  usbh.patch        Enumeration-exclusive control dispatch: while a
                    device enumerates, only address 0, the enumerating
                    address and the hub in use may claim the control
                    slot; LED writes and touch handshakes wait in the
                    pending FIFO (gated at the claim, the two drain
                    checks and the dispatcher, by peeking the head).
                    And a failed enumeration now disables its hub port
                    (CLEAR_FEATURE(PORT_ENABLE), async no-op callback) -
                    0.21 left the abandoned device enabled at address 0,
                    where it answers in parallel with the next device's
                    bring-up.  The 100 ms reset recovery (micropython#3876) stays.

  mp_usbh.c         Mount-callback prints deferred: the callbacks now
  usb_msc.c         format into a small static ring and mp_usbh_task()
                    prints it after tuh_task() returns.  A print in the
                    callback runs the VM via dupterm mid-enumeration -
                    the measured cost of "one connect chime" on the
                    PicoMite rig was the marginal device.  The existing
                    reentrancy guard treated the symptom; this removes
                    the stall itself.

  tusb_config.h     CFG_TUH_CONTROL_PENDING_QUEUE_SZ 4 -> 8: the gate
                    parks more traffic in the FIFO during enumeration.

Not adopted, by the doc's own measurement: the micropython#3533 reference hcd
(fewer devices on marginal hardware; revisit when merged upstream).
Already carried, now cross-validated: MULTI_HUB_FIX and the 64-entry
event queue.  Builds clean; the board leg - all reset kinds, hot
attach, MSC beside HID, pulling the marginal device - is owed before
this is believed.

Co-Authored-By: Claude Fable 5 <[email protected]>
jakub-vesely pushed a commit to jakub-vesely/micropython that referenced this pull request Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py-core Relates to py/ directory in source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants