Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes - #128

Open
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix four latent bugs found by audit, and make the shared-secret check reachableg fixes#128
ispielma wants to merge 5 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown

Five independent bug fixes turned up while Claude audited this repository. Each is a
separate commit so any one can be dropped.

Four are unambiguous defects. The fifth (the last) is a real bug whose fix
changes behaviour for one configuration, and I have flagged it clearly so you
can decide.

1. zlock.py passes an int port to subprocess

get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, when the
labconfig has no [ports] zlock entry, and that value goes straight into an
argv list:

TypeError: expected str, bytes or os.PathLike object, not int

So labscript-zlock cannot start from a labconfig that omits the port.
zlog.py and remote.py already wrap their ports with str().

2. excepthook.set_logger() recurses if called twice

set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second call the
stash captures logwarning itself, so it recurses into itself and the next
warning raises RecursionError. Reachable whenever a process installs the
excepthook more than once. Capturing the real handler once at import is also
idempotent, and stops writing to a private-looking name in warnings.

3. properties._default() cannot serialise np.floating or np.bool_

_default() only handled np.integer. np.float64 survives by accident
because it subclasses Python float, which masked the gap:

int64 ok
float64 ok
float32 TypeError
bool_ TypeError

Any device or plugin writing an np.float32 or np.bool_ into shot
properties fails in serialise().

4. Context.socket() tests against SecureContext, not SecureSocket

The comment directly above says the intent is to insert the security kwargs
only when the socket will be a SecureSocket. The test used SecureContext,
a Context class that can never be a socket class:

issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True

The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended answer either way — which is why this went unnoticed. A
caller explicitly passing SecureSocket silently lost its allow_insecure
configuration.

5. Shared-secret guard is unreachable when allow_insecure is configured — please read

The guard raising _ERR_NO_SHARED_SECRET sits inside the except clause, so
it only runs when [security] allow_insecure is absent. A labconfig that
explicitly sets

[security]allow_insecure = False

with no shared_secret skips it entirely. Inside the except the condition
was also dead weight, since allow_insecure had just been set to False on
the line above.

_ERR_NO_SHARED_SECRET states the intended contract — configure a
shared_secret, or opt out with allow_insecure = True — so explicitly
writing allow_insecure = False without a secret is exactly the
misconfiguration the message exists to catch.

This changes behaviour. An installation with allow_insecure = False and
no shared secret now fails at startup with that message instead of continuing.
Such a setup previously worked as long as all traffic stayed on loopback,
because zprocess only enforces this at send time
(zprocess/security.py). Those users would need to supply a shared secret or
set allow_insecure = True.

It is the last commit on the branch, so drop 635aea9 if you would rather not
take that trade-off; the other four are independent of it.

spielmanand others added 5 commits August 28, 2026 14:44
get_config() falls back to zprocess.zlock.DEFAULT_PORT, an int, whenever
the labconfig has no [ports] zlock entry. That value was placed directly
into the argv list passed to subprocess, which accepts only str, bytes or
os.PathLike:
TypeError: expected str, bytes or os.PathLike object, not int
So labscript-zlock could not start from a labconfig that omits the port.
The sibling launchers zlog.py and remote.py already wrap their ports with
str(); do the same here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_logger() stashed the currently-installed handler into
warnings._showwarning before installing logwarning. On a second
set_logger() call the stash therefore captured logwarning itself, so
logwarning recursed into itself and the next warning raised:
RecursionError: maximum recursion depth exceeded
This is reachable whenever a process installs the labscript excepthook
more than once, such as a subprocess that re-runs application startup or
an app that reconfigures logging.
Capture the real handler once at module import into a module-level name
instead, which is also idempotent and stops writing to a private-looking
attribute of the warnings module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_default() is json.dumps' fallback for objects it cannot encode, and only
handled np.integer. np.float64 survives by accident because it subclasses
Python float, which masked the gap for the other numpy scalar types:
int64 ok
float64 ok
float32 TypeError
bool_ TypeError
Any device or plugin writing an np.float32 or np.bool_ into shot
properties therefore failed in serialise(). Add the two missing branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment directly above states the intent: only insert the
security-related kwargs when the socket is going to be a SecureSocket.
The test used SecureContext, which is a Context class and can never be a
socket class, so the branch was unreachable for any explicit
socket_class:
issubclass(zmq.Socket, SecureContext) = False SecureSocket = False
issubclass(SecureSocket, SecureContext) = False SecureSocket = True
The common pyzmq 25 path, where ThreadAuthenticator passes zmq.Socket,
gives the intended result either way, which is why this went unnoticed.
A caller that explicitly passes SecureSocket, however, silently lost its
allow_insecure configuration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard that raises _ERR_NO_SHARED_SECRET was nested inside the except
clause, so it only ran when [security] allow_insecure was absent from the
labconfig. A labconfig that explicitly sets
[security]
allow_insecure = False
with no shared_secret skipped the check entirely. Inside the except the
condition was also dead weight, since allow_insecure had just been set to
False on the line above.
_ERR_NO_SHARED_SECRET states the intended contract: configure a
shared_secret, or opt out with allow_insecure = True. Explicitly writing
allow_insecure = False without a secret is precisely the misconfiguration
the message exists to catch, so run the guard on both paths.
BEHAVIOUR CHANGE: an installation that explicitly sets allow_insecure =
False with no shared_secret now fails at startup with the message above
instead of continuing. Such a setup previously worked as long as all
communication stayed on loopback, because zprocess only enforces this at
send time (zprocess/security.py). Those users must now either supply a
shared secret or set allow_insecure = True. This commit is deliberately
kept separate so it can be dropped if that trade-off is unwanted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ispielma