Skip to content

Add support for the undocumented bitwise-AND operator & (0xa3) (Part of #135) - #139

Merged
p0dalirius merged 1 commit into
mainfrom
enhancement-bitwise-and-0xa3
Jul 29, 2026
Merged

Add support for the undocumented bitwise-AND operator & (0xa3) (Part of #135)#139
p0dalirius merged 1 commit into
mainfrom
enhancement-bitwise-and-0xa3

Conversation

@p0dalirius

Copy link
Copy Markdown
Collaborator

Linked Issue

Part of #135

Deliberately not a closing keyword. #135 covers two undocumented tokens. 0xfc (@TOKEN.) is implemented in #138; this PR implements 0xa3 (&). 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 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 &. This package was written to the specification, so it rejected the token in both directions: the lexer failed with invalid operator near "&..." and the decoder with unknown conditional-expression token 0xa3.

It is not a stray table entry. & is present in all three copies of the operator table — sechost.dll, advapi32.dll and ntoskrnl.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_14068e214 fetches two operand values and, for token 0xa3 only, 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

File Change
ace/condition/condition.go tokenBitwiseAnd byte = 0xa3 with the semantics and the precedence divergence documented; operatorNames entry "&"
ace/condition/parser.go lone & in lexSymbolOperator (after the && case, so it cannot shadow logical AND); & added to peekRelationalOperator; parseRHS takes the operator and accepts a parenthesized sub-expression for & only
ace/condition/serialize.go relational operands that are logical sub-expressions are parenthesized, via a new relationalOperandText helper
ace/condition/decode.go tokenBitwiseAnd added to isBinaryOperator so the postfix walker pops two operands

encode.go needed no change — it emits BinaryOp generically.

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 with expected 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 groups a & b || c as a & (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, and parseRHS accepts 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:

(@User.flags & 4) from text      REJECT  invalid operator near "& 4)"
0xa3 binary blob                REJECT  unknown conditional-expression token 0xa3

Runtime, after the fix — flag-test forms, text → binary → text, byte-stable:

(@User.a & 1)             -> @User.a & 1              stable=true
(@Resource.flags & 4)     -> @Resource.flags & 4      stable=true
(@User.flags & 0x10)      -> @User.flags & 0x10       stable=true
(@User.a & @User.b)       -> @User.a & @User.b        stable=true
(flags & 3)               -> flags & 3                stable=true

The Windows-grouped tree, decoded from hand-assembled postfix a b c || a3:

-> "@User.a & (@User.b || @User.c)"
re-encodes to the identical bytes: true

Full security-descriptor round-trip, so the operator works inside a real conditional ACE:

D:P(XA;;WP;;;WD;(@Resource.flags & 4))          -> identical
D:P(XA;;WP;;;WD;(@User.a & 1 || @User.b == 2))  -> identical
D:P(XD;;WP;;;WD;(@User.flags & 0x10))           -> identical

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 and go vet reports nothing.

Test Coverage

Added — in ace/condition/condition_test.go:

  • TestBitwiseAnd_0xa3 — asserts the trailing operator byte is 0xa3 and 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 postfix a b c || a3, asserts the parenthesized text, and asserts it re-encodes byte-identically.
  • TestBitwiseAnd_DoesNotShadowLogicalAnd&& still encodes 0xa0, and & applied to two Member_of results (non-integer operands) is rejected.
  • TestBitwiseAnd_ParenthesizedRHSOnlyForBitwiseAnd — the widened right-hand-side grammar applies to & and not to == or <.

Scope of Change

  • Files changed: ace/condition/condition.go, ace/condition/parser.go, ace/condition/serialize.go, ace/condition/decode.go, ace/condition/condition_test.go
  • Submodule pointer updated: no
  • Behavioral changes outside the bug fix: one, and it is required by the fix. relationalOperandText parenthesizes 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 from aclui.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:

  • A latent edge case in Windows itself, read from the decompiled helper and not tested: it signals "not applicable" with 0xffffffffffffffff and the caller treats -1 as 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.
  • 0xa3 cannot 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.

#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
p0dalirius force-pushed the enhancement-bitwise-and-0xa3 branch from e39ee62 to 5cb1b57 Compare July 29, 2026 10:44
@p0dalirius
p0dalirius merged commit d5bed5d into main Jul 29, 2026
5 checks passed
@p0dalirius
p0dalirius deleted the enhancement-bitwise-and-0xa3 branch July 29, 2026 10:46
Sign up for free to 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