Add support for the undocumented bitwise-AND operator & (0xa3) (Part of #135) - #139
Merged
Conversation
#135) Windows implements a conditional-expression operator that MS-DTYP does not document: token 0xa3, spelled '&'. The relational table (2.4.4.17.6) stops at 0x93, the logical table (2.4.4.17.7) defines only 0xa0/0xa1/0xa2, and the 2.5.1.1 ABNF has '&&' but never a lone '&'. A codec written to the specification therefore rejected it: the lexer failed with "invalid operator" and the decoder with "unknown conditional-expression token 0xa3". It is present in all three copies of the operator table (sechost.dll, advapi32.dll and ntoskrnl.exe) and the kernel evaluator implements it: it fetches two operand values and, for token 0xa3 only, returns their bitwise AND, treating a non-zero result as TRUE. So it is a flag test, e.g. (@resource.flags & 4). Parsed here as a binary relational operator, which is what its operand specification says it is - a value-bearing left side with a literal or attribute on the right, the same shape as ==. Adding it alongside && and || instead would have forced both operands to be full expressions and made the flag-test form (@User.a & 1) unparseable, which is the whole point of the operator. One deliberate divergence in the text grammar, documented on the constant: Windows gives '&' precedence 10, the lowest of the binary operators, so it groups 'a & b || c' as 'a & (b || c)'; this package groups it as '(a & b) || c' like every other relational operator. The Windows grouping is ill-typed by '&'s own operand rules, so no meaningful descriptor depends on it. The binary form round-trips either tree faithfully: a '&' with a logical operand now serializes with explicit parentheses, and '&' alone accepts a parenthesized right-hand side so that text re-encodes to the identical bytes.
p0dalirius
force-pushed
the
enhancement-bitwise-and-0xa3
branch
from
July 29, 2026 10:44
e39ee62 to
5cb1b57
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked Issue
Part of #135Deliberately not a closing keyword. #135 covers two undocumented tokens.
0xfc(@TOKEN.) is implemented in #138; this PR implements0xa3(&). Once both land the issue can be closed manually — neither PR alone resolves it. The two are independent and touch different code paths, so they are kept separate rather than combined.Root Cause
Windows implements a conditional-expression operator that MS-DTYP does not document: token
0xa3, spelled&. The relational table (2.4.4.17.6) stops at0x93, the logical table (2.4.4.17.7) defines only0xa0/0xa1/0xa2, and the 2.5.1.1 ABNF has&&but never a lone&. This package was written to the specification, so it rejected the token in both directions: the lexer failed withinvalid operator near "&..."and the decoder withunknown conditional-expression token 0xa3.It is not a stray table entry.
&is present in all three copies of the operator table —sechost.dll,advapi32.dllandntoskrnl.exe, byte-identical — with its own name string distinct from&&, and it is a first-class lexer delimiter in Windows' own tokenizer. The kernel evaluator implements it:FUN_14068e214fetches two operand values and, for token0xa3only, returns their bitwise AND, with the caller treating non-zero as TRUE and zero as FALSE. Full evidence, including the decompiled arm, is in #135.So
&is a bitwise AND flag test:(@Resource.flags & 4)is TRUE exactly when bit 2 is set.Fix Description
ace/condition/condition.gotokenBitwiseAnd byte = 0xa3with the semantics and the precedence divergence documented;operatorNamesentry"&"ace/condition/parser.go&inlexSymbolOperator(after the&&case, so it cannot shadow logical AND);&added topeekRelationalOperator;parseRHStakes the operator and accepts a parenthesized sub-expression for&onlyace/condition/serialize.gorelationalOperandTexthelperace/condition/decode.gotokenBitwiseAndadded toisBinaryOperatorso the postfix walker pops two operandsencode.goneeded no change — it emitsBinaryOpgenerically.The load-bearing design decision:
&is a binary relational operator, not a third logical connective. My first attempt added it as a new outermost precedence level, faithfully mirroring Windows' precedence of 10. That was wrong, and the tests caught it immediately:(@User.a & 1)failed withexpected an attribute name, because a logical level forces both operands to be full expressions.&'s operand specification in the table is the relational shape (identical bytes to==,<,Contains) — a value-bearing left side with a literal or attribute on the right. The flag-test form is the entire purpose of the operator, so it has to parse.The consequent divergence, and why it is safe. Windows' precedence 10 is the lowest of all binary operators (
||is 11,&&is 12), so Windows groupsa & b || casa & (b || c). This package groups it as(a & b) || c, like every other relational operator. That grouping difference only arises for an expression that is ill-typed by&'s own operand rules — the right side would be a logical result rather than an integer, so it cannot evaluate to TRUE — which is why no meaningful descriptor depends on it. It is documented on the constant, and callers who need the exact Windows tree can parenthesize explicitly.Round-trip fidelity is preserved for the Windows tree anyway. Windows' parser can build a
&whose right operand is a logical expression, and postfix encodes that unambiguously. Two changes make such a blob survive binary → text → binary: the serializer parenthesizes a logical operand of a relational operator, andparseRHSaccepts a parenthesized right-hand side for&. That widened grammar is scoped to&—(@User.a == (@User.b || @User.c))still does not parse — with a test pinning that it has not leaked.How Verified
Runtime, before the fix:
Runtime, after the fix — flag-test forms, text → binary → text, byte-stable:
The Windows-grouped tree, decoded from hand-assembled postfix
a b c || a3:Full security-descriptor round-trip, so the operator works inside a real conditional ACE:
Regression check — with the new tests present but the four source files reverted,
go test ./ace/condition/...reports 9 failing cases. With the fix,go test ./...is green across the repository,gofmt -l ace/is clean andgo vetreports nothing.Test Coverage
Added — in
ace/condition/condition_test.go:TestBitwiseAnd_0xa3— asserts the trailing operator byte is0xa3and the text round-trips.TestBitwiseAnd_FlagTestForms— five operand shapes (literal, hex literal, attribute-to-attribute, unprefixed local attribute), each byte-stable. This is the test that failed under the first, logical-level design.TestBitwiseAnd_LogicalOperandRoundTrips— decodes the Windows-grouped postfixa b c || a3, asserts the parenthesized text, and asserts it re-encodes byte-identically.TestBitwiseAnd_DoesNotShadowLogicalAnd—&&still encodes0xa0, and&applied to twoMember_ofresults (non-integer operands) is rejected.TestBitwiseAnd_ParenthesizedRHSOnlyForBitwiseAnd— the widened right-hand-side grammar applies to&and not to==or<.Scope of Change
ace/condition/condition.go,ace/condition/parser.go,ace/condition/serialize.go,ace/condition/decode.go,ace/condition/condition_test.gorelationalOperandTextparenthesizes a logical sub-expression used as an operand of any binary relational operator, not only&. Such a tree is unreachable from this package's text grammar and can only arrive by decoding a binary blob, where the old output would have re-parsed into a different tree; the new output round-trips. No expression that previously serialized correctly changes.Risk and Rollout
Additive in the parse and decode directions: input that previously errored may now succeed, and no existing token's encoding or text form changes. The one shared code path touched is the relational serializer, whose behaviour changes only for operand trees the text grammar cannot express. Safe to merge without staged rollout.
Notes
&is absent fromaclui.dll, so it is unreachable through the Windows security GUI — as is@TOKEN.. Neither construct has been observed in a real-world descriptor; the case for supporting them is that Windows accepts, stores and evaluates them, so a library that reads descriptors from live systems should not fail on one.Not addressed here, and noted for completeness:
0xffffffffffffffffand the caller treats-1as an error, so a genuine AND of two all-ones operands is indistinguishable from an error and collapses to UNKNOWN. Not replicated, since this package does not evaluate conditions.0xa3cannot be satisfied on an AD DS object. It needs integer operands, and AD supplies none —@USER./@DEVICE.claims resolve to UNKNOWN without Dynamic Access Control, and AD silently discards resource-attribute ACEs. Verified live: all four operand combinations denied, with&&and||controls behaving correctly. This does not affect the codec, but it means the operator is only meaningful on file-system or registry objects.